# PRODUCTION READY — BOTFIGHTS > Production sign-off document for the 2-year hardening plan. > All 8 phases complete. Last updated: 2026-03-13. --- ## Test Coverage | Category | Files | Tests | Pass Rate | |----------|-------|-------|-----------| | Server unit/integration | 48 | 690+ | 100% | | Frontend unit | 8 | 78+ | 100% | | E2E (Playwright) | 5 | 11 | 100% | | Soak/stress | 2 | 6 | 100% | | **Total** | **63** | **785+** | **100%** | ### Coverage by Module | Module | Line Coverage | Notes | |--------|-------------|-------| | engine/scoring.ts | 76% | Core scoring logic fully tested | | engine/challenges.ts | 72.3% | All 16 types, 800+ prompts | | engine/answers.ts | 100% | Edge cases, unicode, regex | | engine/odds.ts | 97.2% | Betting odds calculation | | engine/retro-moves.ts | 100% | Choreography retrospective | | middleware/jwt.ts | 75.4% | Create, verify, expiry, tamper | | middleware/nip98.ts | 87.2% | Signature, replay, clock drift | | middleware/rate-limit.ts | 56% | Window, cleanup, eviction | | composables/useFightCache | 95.1% | IndexedDB, LRU, fallback | | composables/useOnlineStatus | 90.9% | Singleton, ref counter | | composables/useFightPolling | 59.8% | SSE reconnect, backoff | --- ## Bugs Fixed (36 total) ### Existing Bugs (BUG-1 through BUG-12) | ID | Description | Status | Regression Test | |----|-------------|--------|-----------------| | BUG-1 | Respond endpoint missing correct/incorrect feedback | Fixed | regression.test.ts | | BUG-2 | Hardcoded 8s timeout instead of challenge.timeout_ms | Fixed | regression.test.ts | | BUG-3 | shuffle() return value discarded | Fixed | regression.test.ts | | BUG-4 | Raw setTimeout() in game code | Fixed | regression.test.ts (pattern check) | | BUG-5 | N+1 queries in fights route | Fixed | regression.test.ts | | BUG-6 | Sequential webhook calls | Fixed | regression.test.ts (pattern check) | | BUG-7 | SSE maps never cleaned | Fixed | regression.test.ts (pattern check) | | BUG-8 | TTS cache FIFO instead of LRU | Fixed | tts-cache.test.ts | | BUG-9 | TODO placeholders in prompts | Fixed | regression.test.ts | | BUG-10 | Sprite fallback drops archetype | Fixed | Code review verified | | BUG-11 | SSE not closed on unmount | Fixed | E2E verified | | BUG-12 | fightEvents.cleanup never called | Fixed | regression.test.ts | ### Server Bugs (BUG-S1 through BUG-S10) | ID | Description | Status | Regression Test | |----|-------------|--------|-----------------| | BUG-S1 | Missing await on drizzle .get() | Fixed | tournaments.test.ts | | BUG-S2 | Race condition in SSE ordering | Fixed | human-responses-ordering.test.ts | | BUG-S3 | Missing rate limit on /poll | Fixed | rate-limit.test.ts | | BUG-S4 | Cashu token validation missing | Fixed | regression.test.ts | | BUG-S5 | JWT_SECRET fallback insecure | Fixed | regression.test.ts | | BUG-S6 | Challenge type enum not enforced | Fixed | regression.test.ts | | BUG-S7 | Unsanitized error responses | Fixed | regression.test.ts | | BUG-S8 | ELO update not atomic | Fixed | regression.test.ts | | BUG-S9 | Rate limit eviction sort-based | Fixed | regression.test.ts | | BUG-S10 | Leaderboard cache full invalidation | Fixed | bots-cache.test.ts | ### Frontend Bugs (BUG-F1 through BUG-F14) | ID | Description | Status | Regression Test | |----|-------------|--------|-----------------| | BUG-F1 | SSE reconnection on disconnect | Fixed | useFightPolling.test.ts | | BUG-F2 | Silent .catch(() => {}) patterns | Fixed | regression.test.ts (pattern check) | | BUG-F3 | feedbackTimer not cleared on unmount | Fixed | HumanFightPage.test.ts | | BUG-F4 | NWC timeout resolves undefined | Fixed | useWallet.test.ts | | BUG-F5 | No ErrorBoundary component | Fixed | ErrorBoundary.test.ts | | BUG-F6 | Array index used as :key | Fixed | Code review verified | | BUG-F7 | autoRestoreRan HMR double-trigger | Fixed | useNostr.test.ts | | BUG-F8 | Relay fetch stops at first relay | Fixed | Code review verified | | BUG-F9 | Polling backoff never escalates | Fixed | useFightPolling.test.ts | | BUG-F10 | Webhook verify fail — user stuck | Fixed | E2E verified | | BUG-F11 | rateLimitTimer not cleaned | Fixed | E2E verified | | BUG-F12 | nip55ReturnHandler not cleaned | Fixed | E2E verified | | BUG-F13 | WebGL contextLost no recovery | Fixed | memory-audit.test.ts | | BUG-F14 | pendingSSEEvents not processed | Fixed | E2E verified | --- ## Security Audit Results ### Hardened Areas - **Input validation**: All POST handlers use Zod schemas via centralized `validators.ts` - **Auth**: NIP-98 + JWT (24h expiry), JWT blacklist for logout, timing-safe bot auth - **Rate limiting**: All mutation endpoints rate-limited, per-IP tracking - **SSRF protection**: Webhook URLs validated against private IP ranges - **Error sanitization**: `sanitizeError()` strips stack traces, file paths, internal errors - **Dependencies**: All pinned (no `^`), MIT/Apache-2.0 only, `pnpm audit` clean - **Docker**: Non-root user, HEALTHCHECK configured - **Secrets**: No secrets in git history, JWT_SECRET required in production ### Known Gaps (Low Risk) - 4 error handlers leak raw `err.message` (bets:118, tournaments:71/92, docs:284) — non-sensitive - 15 async GET handlers lack explicit try/catch — framework catches, returns 500 - /:name route shadows /leaderboard — cosmetic, both work --- ## Scoring & Challenge Quality ### Challenge System - **16 challenge types**, 800+ prompts, all factual scoring - **Difficulty calibration**: Hard prompts added for trivially easy types - **Trap card**: 60 injection resistance prompts - **Answer matching**: Unicode, numeric formats, case-insensitive, regex-safe ### Scoring Formula - **Both correct**: Faster bot gets 7 + speed advantage (0-2), slower gets 5 + ratio (0-1.5) - **Confidence bonus**: Exact match (+0.5-1.0 points) over fuzzy match - **Partial credit**: Wrong answers scored by closeness to correct - **Creative scoring**: Heuristic based on length, vocabulary, structure, spam detection - **Critical hits**: Threshold 3 points margin (lowered from 4) - **Combo system**: Caps at 5x, snowball rate 60.4% (under 70% threshold) ### Competitive Dynamics - **Speed dominance**: 50ms gap = 95% win rate at equal accuracy. This is by design — faster API = better performance - **Tier system**: Well-balanced. Same-tier ~50/50, adjacent 70-93%, 2-tier gap 87-99% - **ELO K=32**: Appropriate calibration, separation reaches 450+ after 50 fights - **Average fight**: 5-8 rounds, 30-70% KO rate --- ## Performance Benchmarks | Metric | Target | Actual | |--------|--------|--------| | Fight throughput (no I/O) | >500/s | >5,000/s | | checkAnswer per call | <1ms | <1ms | | Round scoring | <5ms | <1ms | | 10,000 fight simulation | No crashes | 0 crashes | | Memory (10 replays) | <20% growth | <20% growth | --- ## Deployment Checklist - [ ] Set `JWT_SECRET` environment variable (required in production) - [ ] Set `TRUSTED_PROXY=true` if behind reverse proxy (for rate limit IP extraction) - [ ] Set `FIGHT_LOOP_ENABLED=true` to enable background fight scheduling - [ ] Configure `DATABASE_URL` or ensure SQLite path is writable - [ ] Run `docker build -t botfights .` and verify health endpoint - [ ] Verify non-root user: `docker exec whoami` → `botfights` - [ ] Set `NODE_ENV=production` (Dockerfile does this) - [ ] Verify `pnpm audit --audit-level=high` returns clean --- ## Known Limitations 1. **SQLite**: Single-writer limitation. Not suitable for horizontal scaling without migration to PostgreSQL. 2. **In-memory state**: Active fights, SSE connections, bet escrow are in-memory. Server restart during active fights requires graceful shutdown. 3. **TTS**: 86MB ONNX model loaded in Web Worker. First voice generation has cold start latency. 4. **Speed meta**: When all bots answer correctly, network latency is the primary differentiator. Intended by design but worth noting. 5. **No HTTPS**: Server runs HTTP. Deploy behind reverse proxy (nginx, Caddy) for TLS. 6. **Moderate vulnerabilities**: 3 moderate npm audit findings in transitive dependencies (not exploitable in this context). --- *Signed off by the overnight hardening loop. 785+ tests, 36 bugs fixed, 8 phases complete.*