Dependency: READY — unblocked 2026-08-18. The former #169 blocker was closed as not planned: #168's default now applies to every new ladder, so a scrim ladder stood up today inherits maxQueuedPerTeam = 10 and no backfill is needed. Closes #112 when it lands. See the turnstile correction in the comments before implementing.
Summary
Add POST /api/v1/scrim { taBotSlug, count } so students can run rate-limited practice matches against TA bots server-side instead of relying entirely on the locally-shipped obfuscated binary. See OBFUSCATION_PITCH.md §6 for full design rationale (Option E / J-centric recommendation).
Requirements:
- Resolve the TA-bot team server-side (TA-bot UUIDs never appear in the request/response).
- Enforce an atomic, windowed volume budget — one SQL statement, not read-then-write:
INSERT INTO scrim_usage (team_id, ladder, window_start, count) VALUES (..., 1)
ON CONFLICT (team_id, ladder, window_start)
DO UPDATE SET count = scrim_usage.count + 1 WHERE scrim_usage.count < :cap
RETURNING count;
- Concurrency check must include
created/scheduling states, not just waiting/in_progress (the current Set.of(waiting, in_progress) in GameMatchService.countTeamQueuedMatchesByLadder is a hole a team can sit in).
- Return
Retry-After on rejection.
- Write an audit row (this table doubles as attribution evidence and as the only telemetry available today — there's no actuator, and Prometheus only scrapes
postgres_exporter).
- Carries
@RequireTurnstile.
This is the same fix needed for the existing check-then-act race described in #112 ("I introduced a check-then-act race in the scrimmage rate limiter... we should fix this in the future"). Implement the atomic upsert pattern for both the new scrim budget and the existing rate limiter, and close #112 when this merges.
Suggested starting budget
~20/day, 60/week, burst 2 — then tune from the scrim_usage table rather than debating the number up front.
Summary
Add
POST /api/v1/scrim { taBotSlug, count }so students can run rate-limited practice matches against TA bots server-side instead of relying entirely on the locally-shipped obfuscated binary. SeeOBFUSCATION_PITCH.md§6 for full design rationale (Option E / J-centric recommendation).Requirements:
created/schedulingstates, not justwaiting/in_progress(the currentSet.of(waiting, in_progress)inGameMatchService.countTeamQueuedMatchesByLadderis a hole a team can sit in).Retry-Afteron rejection.postgres_exporter).@RequireTurnstile.Closes #112
This is the same fix needed for the existing check-then-act race described in #112 ("I introduced a check-then-act race in the scrimmage rate limiter... we should fix this in the future"). Implement the atomic upsert pattern for both the new scrim budget and the existing rate limiter, and close #112 when this merges.
Suggested starting budget
~20/day, 60/week, burst 2 — then tune from the
scrim_usagetable rather than debating the number up front.