Skip to content

feat: Bot Arena launch — smart strategy, Polymarket odds, federation, incognito gate#306

Merged
0800tim merged 104 commits into
mainfrom
feat/bot-arena-launch
Jun 7, 2026
Merged

feat: Bot Arena launch — smart strategy, Polymarket odds, federation, incognito gate#306
0800tim merged 104 commits into
mainfrom
feat/bot-arena-launch

Conversation

@0800tim

@0800tim 0800tim commented Jun 7, 2026

Copy link
Copy Markdown
Owner

Summary

Consolidates the Bot Arena work onto main ahead of a prod deploy.

  • Strategy recalibration (browser-swarm + bot-node v0.3.0): group matches favour the stronger side instead of collapsing onto Draw; darling-team picks restricted to FIFA top 16 so cup winners stop surfacing longshots. Verified: 0% longshot champions, home_win is the group favourite.
  • Live Polymarket odds wired into /run via game-service /v1/odds/{match,winner-market,snapshot}, with a FIFA-rank fallback and a source pill in the UI.
  • Federated leaderboard fold: /v1/swarm/totals now sums swarm_claims + swarm_summary, so bot-node containers move the bot-arena headline.
  • Incognito gate on /run: blocks Start in a private window until the operator acknowledges the no-persistence risk or enables Supabase replication.
  • Bot UI cleanup: single-bot bracket drops noisy 2nd/3rd columns; /profile shows a live bot count.
  • billion-bot Docker container: operator dashboard (throttle, throughput, CPU, aggregate-publish status) wrapping @tournamental/bot-node.
  • Developer docs: /bots/node#updating covers updating a pulled container/package.

Merged main cleanly (correct_picks migration, share-page leaderboard, auth deep-link). One semantic merge conflict in leaderboard.ts caught by typecheck and fixed.

Test plan

  • pnpm --filter @vtorn/game typecheck && build clean
  • web tsc --noEmit clean on touched files
  • container generates sensible picks (home_win favourite, no longshot champions)
  • /v1/odds/snapshot serves 72 priced matches + 48-team winner market
  • human review of strategy + UI before prod deploy

🤖 Generated with Claude Code

0800tim and others added 30 commits June 7, 2026 13:46
…ederation)

Captures the design for the Open Bot Arena: 18k cosmetic seed bots,
leaderboard Humans/Bots/My Pools tabs, @tournamental/bot-sdk Node
package, central bulk-insert API, quota auth, leaderboard cache, and
the reference Tournamental Sage bot. Phase 2 preview captures the
federated compute network design: per-node merkle commitments,
pre-kickoff blockchain anchoring, audit verification, and the trust
model. Phase 1 must lay groundwork (merkle tree, committed_at_utc
per pick) so Phase 2 ships as a bolt-on rather than a rewrite.

Tim approved: option A from the brainstorm (Phase 1 central tier
launches by 11 June 2026; Phase 2 federation starts immediately after).

Refs: docs/internal/press-2026-06-07-perfect-bot-bracket/ (gitignored)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: Tim Thomas <0800tim@gmail.com>
21 tasks across 2 streams, TDD pattern, exact file paths, complete
code per step. Phase 2 forward-compat constraints captured in Tasks
5, 10, and the schema migration in Task 2.

Refs: docs/superpowers/specs/2026-06-07-bot-arena-design.md
Signed-off-by: Tim Thomas <0800tim@gmail.com>
Bots are flagged at the auth layer so the prize-eligibility gate and
leaderboard scope filter can short-circuit on a single column read.
Default 0 backfills existing rows safely. Adds insertBotUser() so the
seed CLI and bot SDK mint bot rows without going through the OTP flow.

Refs: docs/superpowers/specs/2026-06-07-bot-arena-design.md §4.1

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: Tim Thomas <0800tim@gmail.com>
… bot_owner, api_key, quota_window, federated_*)

Phase 1 scaffolding for the Open Bot Arena plus the Phase 2 federation
hooks captured up-front so we do not paint ourselves into a corner.
brackets.committed_at_utc lets a future audit reconstruct which kickoff
OTS commitment anchored which picks. federated_node and
federated_leaderboard_snapshot land the schema for the Phase 2
node-operator protocol so endpoints can write before the Docker image
ships.

The file is 0013_bot_arena.sql, not 0009 as the brief said, because
0009 through 0012 are already taken by the syndicate / bracket-import
migrations that shipped this month.

Refs: docs/superpowers/specs/2026-06-07-bot-arena-design.md §8.1, §15.2, §15.6

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: Tim Thomas <0800tim@gmail.com>
Default landing tab is Humans (prize-eligible race). Bots tab shows
AI competitors seeded from a deterministic mock until the live
/api/v1/leaderboard?scope=<audience> endpoint ships. My Pools tab
renders an empty state with deep links to /pools and /syndicates/new
for now; backend wires in a follow-up.

The tab strip lives in a small LeaderboardTabs client component that
owns active state, implements roving-tabindex keyboard nav (Left /
Right / Home / End), and uses role=tablist + aria-selected per W3C
ARIA Authoring Practices. Reuses the existing <Leaderboard> component
with a new optional `scope` prop that flows into a data-scope hook
and, when wired, into the data fetch URL.

Refs: docs/superpowers/specs/2026-06-07-bot-arena-design.md §5
Refs: docs/superpowers/plans/2026-06-07-bot-arena-phase-1.md Task 15
Refs: sessions/2026-06-07_agent-a4_bot-arena-frontend.md
Signed-off-by: Tim Thomas <0800tim@gmail.com>
Phase 1 of the Open Bot Arena. Sets up the public NPM package
@tournamental/bot-sdk with an ESM + CJS dual build via tsup, Apache 2.0
licence matching the repo, vitest harness, and the core type surface
(Outcome, Pick, MatchSpec, BulkSubmission, BulkResponse, OddsSnapshot)
that mirrors the POST /v1/picks/bulk contract.

Refs: docs/superpowers/specs/2026-06-07-bot-arena-design.md
Refs: docs/superpowers/plans/2026-06-07-bot-arena-phase-1.md (Task 11)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: Tim Thomas <0800tim@gmail.com>
postWithRetry handles 429 and 5xx with exponential backoff (base 200ms,
3 attempts) so transient launch-day spikes do not burn the operator's
quota. Bot.pick is idempotent on match_id so re-picking replaces the
outcome, matching the server's ON CONFLICT DO UPDATE semantics. The
queue is preserved on flush error so callers can retry.

Includes submitBulk + submitBulkPicks low-level helpers for power users
packing many bots into one bulk request.

Refs: docs/superpowers/specs/2026-06-07-bot-arena-design.md (sections 6-7)
Refs: docs/superpowers/plans/2026-06-07-bot-arena-phase-1.md (Task 12)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: Tim Thomas <0800tim@gmail.com>
Runs N bots in parallel with default concurrency 16, sized for the
bulk-insert endpoint's 60 req/min budget. Failures inside the per-bot
fn or flush() are counted but do not stop the swarm: large runs prefer
partial completion over an all-or-nothing abort.

Refs: docs/superpowers/specs/2026-06-07-bot-arena-design.md (section 6.4)
Refs: docs/superpowers/plans/2026-06-07-bot-arena-phase-1.md (Task 13)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: Tim Thomas <0800tim@gmail.com>
Each example is self-contained and runs with pnpm tsx examples/<file>.ts.
Covers chalk (baseline), Claude, GPT, swarm, Polymarket arbitrage, Kelly
conviction filter, ensemble vote, and post-tournament best-of (the
card-stacking strategy from spec section 15.1).

README points operators at /bots/keys for self-service issuance and
links to the full /bots/sdk docs page.

Refs: docs/superpowers/specs/2026-06-07-bot-arena-design.md (section 10)
Refs: docs/superpowers/plans/2026-06-07-bot-arena-phase-1.md (Task 14)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: Tim Thomas <0800tim@gmail.com>
Academic emails (.edu, .ac.uk, .ac.nz, .edu.au, .ac.za, .edu.cn,
.ac.jp) get 10x the default quota at issuance: 10,000 bots and 1M
picks per hour. Plaintext key returned only at issuance; subsequent
lookups go through the sha256 hash so a DB leak does not expose any
callable keys.

Refs: docs/superpowers/specs/2026-06-07-bot-arena-design.md §6.3, §14

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: Tim Thomas <0800tim@gmail.com>
Scaffolds apps/operator-swarm/ so Tim can run a 1M-bot Tournamental Bot
Node on the prod box under the 0800tim user with PM2 supervision,
auto-restart, log rotation, and a localhost /stats health probe. The
wrapper does no runtime logic: it just schedules the
@tournamental/bot-node CLI (A3) and manages the operator credentials
lifecycle. This guarantees the public Bots leaderboard tab has real
federated activity from day one of the FIFA World Cup 2026 launch.

Sizing: 12 GB heap covers the documented ~10 GB peak at 1M bots with
~2 GB headroom. Scale up via --max-old-space-size, or shard across
multiple OPERATOR_NODE_LABEL instances past ~2M bots per process.

Files:
- apps/operator-swarm/package.json (scripts: register, start, stop,
  restart, reload, status, logs, health, save, delete)
- apps/operator-swarm/.env.example (six brief vars plus
  BOT_NODE_STATS_PORT and OPERATOR_CREDENTIALS_PATH)
- apps/operator-swarm/scripts/register.sh (idempotent, validates JSON,
  chmods credentials 600)
- apps/operator-swarm/ecosystem.config.cjs (PM2 fork mode, 12 GB heap,
  rotated logs to logs/)
- apps/operator-swarm/scripts/health-check.sh (curl /stats, human/json/
  quiet modes, stale-commit detection)
- apps/operator-swarm/README.md (full deploy guide for the server
  admin, sizing table, troubleshooting)
- apps/operator-swarm/.gitignore (logs/, .env, dump.pm2)

Refs: docs/superpowers/specs/2026-06-07-bot-arena-design.md
Refs: docs/superpowers/plans/2026-06-07-bot-arena-phase-1.md
Refs: sessions/2026-06-07_A8_operator-swarm-scaffold.md

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: Tim Thomas <0800tim@gmail.com>
BotOwnerStore ties a bot user to its issuing API key and powers the
ownership guard on /v1/picks/bulk. QuotaStore enforces the per-key
picks-per-hour cap via a (api_key_hash, window_start) composite key
and an atomic upsert-with-add on consume. tryConsume() refuses
single-request payloads larger than the cap up front so a misconfigured
key cannot exhaust its quota in one shot.

Refs: docs/superpowers/specs/2026-06-07-bot-arena-design.md §6.4, §7.2

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: Tim Thomas <0800tim@gmail.com>
Brings in apps/operator-swarm/ (Tim's 1M-bot federated swarm node
PM2 wrapper) on top of the integration branch.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Runs every 6 hours under PM2, reads Polymarket odds via the apps/odds-ingest
snapshot endpoint, asks Claude Opus 4.7 for a per-match decision, posts via
@tournamental/bot-sdk. Reserved handle @Sage. Demonstration bot for the SDK
launch; competes publicly on the Bots leaderboard tab.

Sage exits after one decision pass so PM2's cron_restart drives the cadence.
Strategy module has deterministic fallbacks: parser rejects anything other
than the three canonical tokens and decide() falls back to the favourite
implied by the odds (with home_win tie-break). Tests cover parser strictness,
favourite selection, prompt construction, and the Claude-returns-garbage and
Claude-throws fallback paths.

Env vars: ANTHROPIC_API_KEY, TOURNAMENTAL_API_KEY, optional TOURNAMENTAL_BOT_ID
(otherwise Sage calls /v1/bots/register once and caches in .sage-state.json).

Refs: docs/superpowers/specs/2026-06-07-bot-arena-design.md §9
Refs: docs/superpowers/plans/2026-06-07-bot-arena-phase-1.md Task 20

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: Tim Thomas <0800tim@gmail.com>
Persists the registry (federated_node) and per-(node, match) aggregate
snapshots (federated_leaderboard_snapshot). Supports the three Phase 2
endpoints: register (issue node credentials), commit (pre-kickoff
merkle root), reportLeaderboard (post-match aggregate + top-K).

The commit + reportLeaderboard upserts merge cleanly so out-of-order
delivery (a node reports leaderboard before the central tier has seen
its commit) does not lose data. listFederatedTopK powers the
?source=federated query on the public leaderboard.

Refs: docs/superpowers/specs/2026-06-07-bot-arena-design.md §15.2

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: Tim Thomas <0800tim@gmail.com>
New @tournamental/bot-node package: a runnable Node.js + SQLite app any
operator (including Tim's million-bot swarm) clones, registers, and runs
to commit merkle roots to the central server pre-kickoff and report
aggregate leaderboards post-match.

Highlights:
- CLI: register, generate, commit, score, serve, plus a one-shot
  --bots=N --dry-run smoke path.
- better-sqlite3 storage in WAL mode, prepared statements, batched
  transactions; sized for 1M bots on a 32-core box.
- sha256 sorted-pair merkle tree matching the central server shape, with
  per-bot proof endpoint exposed via Fastify on /v1/proof/:match/:bot.
- Deterministic chalk strategy (chalk_score uniform in [0.65, 0.90]) and
  pluggable Strategy interface for Claude/GPT/custom plugins.
- Dockerfile + docker-compose.yml for one-command operator deploy.
- 16 vitest cases covering merkle, generator determinism, and scorer
  flow; build, typecheck, tests, and dry-run all green.

Refs: docs/superpowers/specs/2026-06-07-bot-arena-design.md section 15

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: Tim Thomas <0800tim@gmail.com>
Builds a sorted-pair SHA-256 merkle tree over (bot_id, match_id,
outcome, locked_at) leaves. Phase 1 uses it inside the kickoff OTS
commitment so the on-chain hash is already a merkle root, not a flat
sha256; Phase 2 federated nodes mirror the same construction so adding
their leaves to the audit tree is a matter of adding inputs, not
changing the verifier shape.

Sorted-pair (rather than RFC 6962-style positional) keeps the verifier
small enough to port to any language in 50 lines and matches the
OpenZeppelin MerkleProof.sol shape so a Phase 3 on-chain verifier ports
trivially.

Refs: docs/superpowers/specs/2026-06-07-bot-arena-design.md §15.6

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: Tim Thomas <0800tim@gmail.com>
Adds @tournamental/bot-mcp, a Model Context Protocol server that lets
Claude Desktop, Cursor, and any MCP-compatible AI client play in the
FIFA World Cup 2026 prediction tournament conversationally.

Six tools wrap the public api.tournamental.com surface defined in
docs/superpowers/specs/2026-06-07-bot-arena-design.md:

  - get_matches:     104-match catalogue with kickoff times
  - get_odds:        Polymarket-style odds snapshot per match
  - get_my_bots:     bots owned by the configured API key + quota
  - submit_pick:     single-pick submission (wraps the bulk endpoint)
  - submit_bulk:     up to 10,000 picks across up to 1,000 bots, atomic
  - get_leaderboard: humans / bots / pools tabs

Boots over stdio via the standard @modelcontextprotocol/sdk transport
so any MCP client config of the form `command: npx, args: [-y,
@tournamental/bot-mcp]` works out of the box. Bundled
example-claude-desktop-config.json is the drop-in form.

API key is read from TOURNAMENTAL_API_KEY in the spawned-process env;
TOURNAMENTAL_BASE_URL is an optional override for staging.

16 tests cover each tool's request shape against a fake fetch plus an
end-to-end MCP server roundtrip over InMemoryTransport that asserts
tools/list reports exactly the six expected tools.

Refs: docs/superpowers/specs/2026-06-07-bot-arena-design.md §3 Phase 2

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: Tim Thomas <0800tim@gmail.com>
apiKeys, botOwners, quotas, federatedNodes hang off the store so the
rest of the service does not need to know which file or migration
laid the SQLite down.

topNByScope(tournamentId, scope, n) returns humans-only, bots-only,
or everyone for the new tabbed leaderboard. The new prepared
statements partition cleanly so the cache key per tab does not blow
up the LRU.

Refs: docs/superpowers/specs/2026-06-07-bot-arena-design.md §5, §6

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: Tim Thomas <0800tim@gmail.com>
Ship the public Bot Arena API contract as OpenAPI 3.0.3 at
docs/openapi/bot-arena.yaml covering /v1/picks/bulk,
/v1/leaderboard?scope=, /v1/leaderboard/my-pools,
/v1/bots/keys/issue, and the Phase 2 /v1/nodes/* federated
node protocol with full request/response schemas, auth, and
error responses.

Cross-link the existing Humanness Score policy in doc 20
to the Bot Arena design spec, implementation plan, developer
guide source, and public /developers + /bots/sdk pages so the
two policy surfaces interlock cleanly.

Press refresh (lead angle shift to "Can a human beat a bot?"),
university outreach plan (32 departments + 8 AI labs + 5
prediction-market communities + 20 social commentators),
developer guide content, and dev-guide press release all live
at docs/internal/ per the gitignored internal-docs policy.

Refs: docs/superpowers/specs/2026-06-07-bot-arena-design.md
Refs: docs/20-identity-humanness-bots.md

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: Tim Thomas <0800tim@gmail.com>
Editorial doc page covering the eight sections from spec §10:
quickstart, architecture, API reference, bulk-insert, quotas, live
data feeds, eight worked examples, and FAQ. Style mirrors /the-bet
so the developer micro-site reads as one consistent surface, with
the gold accent on the API surface so it pops as the thing of value.

Sticky on-page TOC anchors each section by stable id (#quickstart,
#architecture, #api-reference, #bulk-insert, #quotas, #feeds,
#examples, #faq). Eight-question FAQ covers cash ineligibility, LLM
legality, OTS verification, federated nodes, licence, and feature
requests so a press visitor can answer their first ten questions
without leaving the page.

Tests assert the eight stable section ids exist and that the page
cross-links to /bots/keys and /bots/node so the SDK micro-site stays
internally connected.

Refs: docs/superpowers/specs/2026-06-07-bot-arena-design.md §10
Refs: docs/superpowers/plans/2026-06-07-bot-arena-phase-1.md Task 16
Refs: sessions/2026-06-07_agent-a4_bot-arena-frontend.md
Signed-off-by: Tim Thomas <0800tim@gmail.com>
Server component gates issuance behind the inbound session cookie,
resolves the verified email via loadUserContact(), and proxies
issuance to the game-service /v1/bots/keys/issue endpoint. The
plaintext key is forwarded to the browser once; the server only
persists the SHA-256 hash so a lost key cannot be recovered (issue
a new one + revoke the old via info@tournamental.com).

Client form is a single labelled input + submit + result panel.
Validates label charset and length client-side and server-side so
the upstream call always has a sanitised payload. Empty / overlong
/ garbage labels short-circuit at the proxy with a 400 before any
upstream traffic.

Unauthenticated visitors see a sign-in gate with a /login deep link
that round-trips back to /bots/keys after auth.

Refs: docs/superpowers/specs/2026-06-07-bot-arena-design.md §6.3
Refs: docs/superpowers/plans/2026-06-07-bot-arena-phase-1.md Task 17
Refs: sessions/2026-06-07_agent-a4_bot-arena-frontend.md
Signed-off-by: Tim Thomas <0800tim@gmail.com>
The /developers landing surface routes to every developer-facing
destination in one place: /bots/sdk, /bots/keys, /bots/node, /run,
GitHub, NPM, MCP. Two grouped cards-on-cards lists (On Tournamental
+ Off-site) with phase badges (Live, Phase 1, Phase 2, Beta) so a
visitor can scan readiness at a glance.

/bots/node is the federated bot-node operator guide per spec §15:
why federate, quickstart (Docker compose when Phase 2 lands), the
pre-kickoff commitment flow, post-match aggregation, third-party
verification, the four audit constraints, and a capacity-planning
table covering 100k -> 1B bot ranges.

MORE_DESKTOP nav gets a Bot Arena entry pointing at /developers,
matching prefix /developers. Active-route highlighting works the
same as every other More entry.

Reuses /bots/sdk styles for /bots/node so the developer micro-site
reads as one consistent editorial canvas.

Refs: docs/superpowers/specs/2026-06-07-bot-arena-design.md §15
Refs: sessions/2026-06-07_agent-a4_bot-arena-frontend.md
Signed-off-by: Tim Thomas <0800tim@gmail.com>
Aligns the public house-prize terms with the Open Bot Arena launch.
New section 4a sits between Eligibility (4) and The Bracket (5),
anchored at #bots so the SDK page (and any future press) can deep
link to the clause.

States:
  - bots welcome to compete on a separate leaderboard tab
  - cash Prize ineligibility (Humanness Score >= 50 required)
  - bot Humanness is 0 by design
  - perfect-bracket recognition is non-cash: badge + research
    co-author invitation + non-monetary trophy
  - operators disclose ownership at API key issuance and operate
    within published quotas

Tests assert the #bots anchor, the cash-ineligibility statement,
the 50-point Humanness floor, and the three non-cash recognition
items. The /bots/sdk cross-link is verified so the terms surface
stays self-consistent with the SDK micro-site.

Refs: docs/superpowers/specs/2026-06-07-bot-arena-design.md §11
Refs: docs/superpowers/plans/2026-06-07-bot-arena-phase-1.md Task 18
Refs: sessions/2026-06-07_agent-a4_bot-arena-frontend.md
Signed-off-by: Tim Thomas <0800tim@gmail.com>
Validates payload via Zod (10k pick + 1k submission ceiling), checks
the API key, verifies ownership of every referenced bot_id in one
SQL round trip, charges the hourly quota up-front, and commits the
upsert inside a single SQLite transaction with a prepared statement
reused across rows. 10k picks lands in ~80ms on the dev box,
comfortably inside the 500ms p99 budget.

Per-bot existing brackets are merged so a re-submit does not wipe
prior picks. Numeric match_ids land on matchPredictions (group stage);
alphanumeric ids land on knockoutPredictions, matching the WC2026
fixture catalogue shape.

Refs: docs/superpowers/specs/2026-06-07-bot-arena-design.md §7

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: Tim Thomas <0800tim@gmail.com>
Outcome summary, file inventory, and dependencies for the Phase 1
Bot Arena frontend agent (A4). 5 prior commits land the leaderboard
tabs, /bots/sdk docs, /bots/keys issuance, /bots/node operator
guide, /developers hub, terms clause, and nav entry.

Refs: docs/superpowers/specs/2026-06-07-bot-arena-design.md
Refs: docs/superpowers/plans/2026-06-07-bot-arena-phase-1.md
Signed-off-by: Tim Thomas <0800tim@gmail.com>
Handler is attached to the tab button, not the tablist div. Tighten
the generic so tsc passes and the event is correctly typed for any
future button-only event reads.

Refs: docs/superpowers/specs/2026-06-07-bot-arena-design.md §5
Signed-off-by: Tim Thomas <0800tim@gmail.com>
Browser-first hook page for the Open Bot Arena. Leads with "spawn a
million unique bots in your browser", walks through the 5-step zero-
install setup, explains the six swarm-tuning sliders (chalk bias, draw
bias, upset rate, update cadence, LLM strategy, bot count), and
guarantees within-swarm bracket uniqueness with probability-mass
spread.

Three runtimes (browser default, Node SDK, federated Node operator)
share one federated protocol, one merkle commitment shape, one
blockchain audit trail, and one uniqueness guarantee. Live leaderboard
comparison (Humans vs Bots vs My Pools) is the central story.

Nav: added Bot Arena to MORE_DESKTOP (between Match calendar and
About) and DRAWER_PRIMARY (between Match calendar and Pools).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: Tim Thomas <0800tim@gmail.com>
…terms clause)

Signed-off-by: Tim Thomas <0800tim@gmail.com>

# Conflicts:
#	apps/web/components/shell/nav-links.tsx
…rboard)

Phase 2 forward-compat surface. External node operators register a
public URL + owner email under their bot-owner API key; the central
tier mints a separate node credential (also tnm_-prefixed) bound to a
new node_id. Pre-kickoff merkle commits land via /commit; post-match
aggregate reports + top-1000 rows land via /leaderboard.

Auth scheme reuses the api_key table so owner credentials and node
credentials share the same hash + revocation surface. /commit and
/leaderboard refuse when the auth key does not own the referenced
node_id (spec §15.3 audit). /commit enforces the strict pre-kickoff
invariant: kickoff_at must be in the future or the call is a 422.

The Phase 1 build ships these endpoints empty-data so external node
operators can wire clients before the Phase 2 Docker image goes
public, and so the central tier can integration-test the merge into
the federated leaderboard view.

Refs: docs/superpowers/specs/2026-06-07-bot-arena-design.md §15.2, §15.3

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: Tim Thomas <0800tim@gmail.com>
0800tim and others added 29 commits June 7, 2026 20:39
The Next.js proxy at /api/v1/bots/keys already pointed at
/v1/bots/keys/issue on the game-service, but that endpoint did not
exist. Anyone hitting the self-serve Generate-key button got a 404
unless they happened to have a Supabase session, in which case the
older /v1/me/api-keys path picked them up.

This commit ships the missing endpoint. It accepts a service-to-service
shared secret in the x-bot-keys-shared-secret header (env:
GAME_BOT_KEYS_SHARED_SECRET on both ends), validates the body via Zod,
defers to ApiKeyStore.issue() for the actual mint, and returns the
plaintext key once. Fails closed with 503 when the env var is unset so
an accidentally-deployed-without-secret build is not a free key-mint
endpoint.

Quotas inherit from ApiKeyStore.issue (.edu / .ac.uk / .ac.nz /
.edu.au / .ac.za / .edu.cn / .ac.jp get 10x). Constant-time-ish secret
compare avoids leaking matched-prefix length.

The Next.js proxy now sends both x-bot-keys-shared-secret AND the
legacy X-Tournamental-Service header so older game-service builds
without the new endpoint keep working through the rollout.

10 new endpoint tests cover the happy path, academic quota lift, all
four error codes, and an end-to-end smoke that the freshly minted key
authenticates against /v1/picks/bulk.

Refs: apps/web/app/api/v1/bots/keys/route.ts

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: Tim Thomas <0800tim@gmail.com>
/profile/api-keys silently no-opped for SMS-OTP and Telegram users
because the page only knew how to call Supabase /v1/me/api-keys, and
those auth flows don't mint Supabase sessions on vtorn-dev.

Add a mintApiKeyViaCookie() client that POSTs to /api/v1/bots/keys
(the existing Next.js proxy that authenticates via the inbound-session
cookie). When browserClient() returns null, ApiKeysPage now uses the
cookie path instead of surfacing an error banner.

Also fix the secondary bug where refresh() left the loading flag on
forever when Supabase was unconfigured, so the page got stuck on
"Loading...".

UX matches the Supabase path 1:1: the freshly minted key renders in
the same FreshKeyBanner with copy-to-clipboard. The key list stays
empty on the cookie path until the game-service exposes a
list-my-bot-keys surface (out of scope for tonight).

Refs: apps/web/app/api/v1/bots/keys/route.ts

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: Tim Thomas <0800tim@gmail.com>
provenance: true only works under GitHub Actions OIDC. Manual publish
from a laptop fails with "Automatic provenance generation not
supported for provider: null". Drop the flag so the manual ship path
works tonight. Re-add via CI workflow when we set one up.

Signed-off-by: Tim Thomas <0800tim@gmail.com>
…ofile + perfect-track)

Four pieces of plumbing so each operator running a browser swarm or
Node bot-node publishes summaries to a Cloudflare-edge-cached endpoint,
and the platform surfaces a fire badge when any bot survives match 80
on a perfect track.

A. POST /v1/swarms/<operator_id>/summary
   * operator_id = sha256(api_key) so no second identity column.
   * Bearer auth; 401 missing, 401 invalid, 403 not_operator on mismatch.
   * Idempotent upsert on (operator_id, kickoff_at).
   * Persists into new swarm_summary table.

B. GET /v1/swarms/<operator_id> and GET /v1/swarms
   * Public, no auth.
   * Cache-Control: public, s-maxage=60, stale-while-revalidate=300
   * ETag based on latest generated_at; 304 fast path.
   * Cloudflare edge caches without touching origin.
   * Global (no operator) returns top 100 by best_bot_score.

C. /profile/[handle]/swarm
   * Tab on the profile page: total bots, best score, sparkline.
   * "Download swarm summary (JSON)" button hits the cached endpoint.
   * Own profile only: "Download my raw bot brackets (JSON)" pulls
     from IndexedDB, no server call. Raw picks of other users stay
     private.

D. perfect_track_alert table + watch service
   * Inline trigger after every POST /v1/swarms/<id>/summary.
   * Also after every admin match-settled event in routes/match.ts.
   * Webhook fan-out via PERFECT_TRACK_WEBHOOK_URL (no-op if unset).
   * <PerfectTrackBadge/> on /leaderboard surfaces a 🔥 N bots still
     on a perfect track after match X.

Browser swarm integration:
* BrowserSwarm.tsx calls publishOperatorSummary after each batch.
* federation.ts gains the publish helper; soft-fails on network error
  with no user-visible warning.
* persistence.ts adds operator_api_key store; no schema bump because
  it lives in the existing STORE_DEVICE namespace.

Tests:
* 257/257 game tests pass (was 234; 23 new across routes + service).
* No em-dashes anywhere.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: Tim Thomas <0800tim@gmail.com>
…ovenance for manual publish

After this evening's npm publish blitz, all 8 @tournamental packages
now live on the registry:
  @tournamental/spec           0.2.0  (was 0.1.0)
  @tournamental/bracket-engine 0.2.0  (was 0.1.0)
  @tournamental/plugin-sdk     0.2.0  (was 0.1.0)
  @tournamental/social-cards   0.2.0  (was 0.1.0)
  @tournamental/bot-sdk        0.1.0  (first publish)
  @tournamental/bot-node       0.1.0  (first publish)
  @tournamental/bot-mcp        0.1.0  (first publish)
  @tournamental/create-app     0.1.0  (first publish)

provenance: true removed from publishConfig because the manual publish
path can't generate OIDC provenance attestations. Re-add via a CI
workflow if/when we set one up.

Signed-off-by: Tim Thomas <0800tim@gmail.com>
The /run/bots/[index] detail page used to show knockout slot
placeholders (winner_grpA, annex_third_vs_grpB, ...) for all 32
knockout matches. The bracket-engine cascade is declarative so each
bot's knockouts can be resolved per-bot from its 72 group-stage
Outcome picks.

cascade.ts builds a BracketPrediction from the bot's per-match picks:
group standings via bracket-engine's computeGroupStandings (which
already handles points -> GD -> GF -> head-to-head), 8 best-thirds
chosen by FIFA-rank (lower = better third), then a forward walk
through knockouts that re-cascades after each round so r16 onwards
see the resolved upstream winners.

The detail page now renders France vs Argentina (or whoever this
bot thinks gets through) in every knockout row.

Unit tests cover bot 0, bot 12345, the predicted-winner of the
final, determinism across repeat calls, and the
resolvedKnockoutSlots helper the detail page consumes.

Refs: docs/superpowers/specs/2026-06-07-bot-arena-design.md
Refs: sessions/2026-06-07_a11_phase2-polish.md

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: Tim Thomas <0800tim@gmail.com>
Two Phase 2 polish features land together because they share the
worker hot path and the BrowserSwarm runner UI.

uniqueness.ts: index-based deviation enumeration. Bot 0 is the pure
chalk bracket (favourite outcome for every match). Bots 1..S each
flip one outcome relative to chalk, ordered by ascending confidence
margin (cheapest deviation first). Bots S+1.. cover all
combinations of two flips, then three, etc., via standard
lexicographic combinatorial unranking. Two distinct bot indices in
the same operator scope are now GUARANTEED structurally distinct
bracket commits, not merely probabilistically.

anchor.ts: user-anchored swarm slider. Reads the user's saved
bracket from localStorage on every Start press and blends per-match
picks with chalk by a slider weight: Off (0) / Soft (0.4) /
Strong (0.75) / Lockstep (1.0). Slider position persists to
swarm_state.anchor_weight; the bracket-hash snapshot of each batch
persists to swarm_state.last_anchor_hash so committed batches stay
locked to the snapshot they used while the next batch picks up the
live bracket.

Worker hot path now: perturbedOutcome() then optional blendOutcome()
overlay. Detail page swaps from regenerateBotBracket to
regenerateBotBracketUnique so the rendered bracket matches the
committed one. Cascade resolver also uses perturbation so the
knockout tree on /run/bots/[index] matches what landed on the
federated leaderboard.

Persistence schema extends SwarmState with two new fields
(anchor_weight, last_anchor_hash); fresh DBs start with anchor_weight
0 and last_anchor_hash null so existing browser DBs upgrade
transparently.

Unit tests:
  - uniqueness: 6 tests covering pure chalk, single-deviation
    coverage, structural distinctness across 200 bots, double-
    deviation level boundary.
  - anchor: 7 tests covering weight constants, blend boundaries at
    weight = 0 / 1 / intermediate, flattenBracket, captureSnapshot
    determinism.

Refs: docs/superpowers/specs/2026-06-07-bot-arena-design.md
Refs: sessions/2026-06-07_a11_phase2-polish.md

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: Tim Thomas <0800tim@gmail.com>
Captures the three deliverables (cascade resolver, uniqueness
perturbation, user-anchored slider), the test surface (19 unit
tests across 3 suites), and the typecheck outcome.

Refs: docs/superpowers/specs/2026-06-07-bot-arena-design.md

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: Tim Thomas <0800tim@gmail.com>
…loop mode

Tim 2026-06-07 evening, four changes for the prod launch tonight:

1. Desktop nav swap: Bot Arena promoted to primary nav after Save &
   share. The Bet drops to More. Bot Arena is the story this week so
   it gets the one-click visibility from every page.

2. Storage card on /run: IndexedDB is now the always-on default with
   a green badge. New tick-box "Also replicate to Supabase" reveals
   the URL + anon key fields only when chosen, plus an inline FAQ
   with 4 setup steps and a privacy note ("we only use the public
   anon key, never the service role key").

3. Strategy card on /run: vendor cascade lets users bring their own
   LLM. Supports Anthropic (Claude Opus 4.7 / Sonnet 4.6 / Haiku 4.5),
   OpenAI (GPT-4o / 4o-mini / o1-mini), OpenRouter (free-text model
   field, defaults to Llama 3.1 405B), and Google Gemini (2.0 Flash
   or 1.5 Pro). Per-vendor key URLs are surfaced so users know where
   to get a key.

4. Run card on /run: new "Keep looping" tick box with infinity glyph.
   Generates botCount bots, commits the merkle root, then repeats
   automatically until the user clicks Stop loop. Warning copy
   surfaces if loop is on at >= 100k bots per batch ("CPU stays
   busy continuously, laptop will warm up, leave it overnight").
   Iteration counter shown under the button. Stop tells the loop
   driver to halt after the current batch finishes.

The loop driver is a useEffect that fires when phase flips to "done"
and loopMode is true; it schedules the next onStart() via setTimeout
with a stopRequestedRef gate so onStop cleanly cancels mid-loop.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: Tim Thomas <0800tim@gmail.com>
… key + add nav.bot_arena translation

Two bugs Tim caught while testing /profile/api-keys on dev:

1. nav.bot_arena translation key did not exist in apps/web/locales/en.json
   so the DesktopNav NavPill for Bot Arena spammed
   IntlError: MISSING_MESSAGE in the console (safeT caught it with a
   fallback so the label still rendered, but the noise is bad). Added
   the en.json entry.

2. Generate key button silently no-op'd on dev. The handleMint
   shortcircuits with `if (!sb) return;` when the Supabase browser
   client cannot initialise, which is the case on vtorn-dev because
   that environment authenticates via SMS-OTP / Telegram, not
   Supabase. No setError call, so the user saw nothing happen and no
   network activity. Surface a clear "Supabase auth client is not
   initialised" error message that points them at the working
   self-serve flow at /bots/keys.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: Tim Thomas <0800tim@gmail.com>
The /bots/sdk documentation page promised five SDK methods the code did
not actually ship. Plug the gaps so the quickstart in the docs works
verbatim:

- Bot.connect() authenticates the API key against /v1/me/api-keys/whoami,
  warms the match catalogue from /v1/tournaments/<id>/matches, and
  returns { matches, authenticated } for log lines. Cheap, idempotent;
  401 fails fast, 404 / network errors degrade to a cache-only connect.
- Bot.matches() iterates the cached catalogue, filtering out matches
  whose kickoff_utc has already passed against a caller-supplied
  (or wall-clock) now.
- Bot.setCatalogue() seeds the iterator for tests / federated fixtures.
- getOdds(matchId) calls /v1/odds/<matchId>, accepts the documented
  nested-probabilities shape as well as the flat shape, synthesises a
  deterministic favourite via argmax, and falls back to a no_odds
  50/50 result on a network error so a chalk swarm still ships.
- getInjuries and getWeather are Phase 1 stubs with stable signatures
  so consumer code wires now and the real feed lands later with zero
  caller changes.

README quickstart updated to match the docs page; 17 new unit tests
cover happy paths, fallbacks, and Bot.matches kickoff filtering.

Refs: apps/web/app/bots/sdk/page.tsx

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: Tim Thomas <0800tim@gmail.com>
The Next.js proxy at /api/v1/bots/keys already pointed at
/v1/bots/keys/issue on the game-service, but that endpoint did not
exist. Anyone hitting the self-serve Generate-key button got a 404
unless they happened to have a Supabase session, in which case the
older /v1/me/api-keys path picked them up.

This commit ships the missing endpoint. It accepts a service-to-service
shared secret in the x-bot-keys-shared-secret header (env:
GAME_BOT_KEYS_SHARED_SECRET on both ends), validates the body via Zod,
defers to ApiKeyStore.issue() for the actual mint, and returns the
plaintext key once. Fails closed with 503 when the env var is unset so
an accidentally-deployed-without-secret build is not a free key-mint
endpoint.

Quotas inherit from ApiKeyStore.issue (.edu / .ac.uk / .ac.nz /
.edu.au / .ac.za / .edu.cn / .ac.jp get 10x). Constant-time-ish secret
compare avoids leaking matched-prefix length.

The Next.js proxy now sends both x-bot-keys-shared-secret AND the
legacy X-Tournamental-Service header so older game-service builds
without the new endpoint keep working through the rollout.

10 new endpoint tests cover the happy path, academic quota lift, all
four error codes, and an end-to-end smoke that the freshly minted key
authenticates against /v1/picks/bulk.

Refs: apps/web/app/api/v1/bots/keys/route.ts

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: Tim Thomas <0800tim@gmail.com>
/profile/api-keys silently no-opped for SMS-OTP and Telegram users
because the page only knew how to call Supabase /v1/me/api-keys, and
those auth flows don't mint Supabase sessions on vtorn-dev.

Add a mintApiKeyViaCookie() client that POSTs to /api/v1/bots/keys
(the existing Next.js proxy that authenticates via the inbound-session
cookie). When browserClient() returns null, ApiKeysPage now uses the
cookie path instead of surfacing an error banner.

Also fix the secondary bug where refresh() left the loading flag on
forever when Supabase was unconfigured, so the page got stuck on
"Loading...".

UX matches the Supabase path 1:1: the freshly minted key renders in
the same FreshKeyBanner with copy-to-clipboard. The key list stays
empty on the cookie path until the game-service exposes a
list-my-bot-keys surface (out of scope for tonight).

Refs: apps/web/app/api/v1/bots/keys/route.ts

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: Tim Thomas <0800tim@gmail.com>
provenance: true only works under GitHub Actions OIDC. Manual publish
from a laptop fails with "Automatic provenance generation not
supported for provider: null". Drop the flag so the manual ship path
works tonight. Re-add via CI workflow when we set one up.

Signed-off-by: Tim Thomas <0800tim@gmail.com>
…ofile + perfect-track)

Four pieces of plumbing so each operator running a browser swarm or
Node bot-node publishes summaries to a Cloudflare-edge-cached endpoint,
and the platform surfaces a fire badge when any bot survives match 80
on a perfect track.

A. POST /v1/swarms/<operator_id>/summary
   * operator_id = sha256(api_key) so no second identity column.
   * Bearer auth; 401 missing, 401 invalid, 403 not_operator on mismatch.
   * Idempotent upsert on (operator_id, kickoff_at).
   * Persists into new swarm_summary table.

B. GET /v1/swarms/<operator_id> and GET /v1/swarms
   * Public, no auth.
   * Cache-Control: public, s-maxage=60, stale-while-revalidate=300
   * ETag based on latest generated_at; 304 fast path.
   * Cloudflare edge caches without touching origin.
   * Global (no operator) returns top 100 by best_bot_score.

C. /profile/[handle]/swarm
   * Tab on the profile page: total bots, best score, sparkline.
   * "Download swarm summary (JSON)" button hits the cached endpoint.
   * Own profile only: "Download my raw bot brackets (JSON)" pulls
     from IndexedDB, no server call. Raw picks of other users stay
     private.

D. perfect_track_alert table + watch service
   * Inline trigger after every POST /v1/swarms/<id>/summary.
   * Also after every admin match-settled event in routes/match.ts.
   * Webhook fan-out via PERFECT_TRACK_WEBHOOK_URL (no-op if unset).
   * <PerfectTrackBadge/> on /leaderboard surfaces a 🔥 N bots still
     on a perfect track after match X.

Browser swarm integration:
* BrowserSwarm.tsx calls publishOperatorSummary after each batch.
* federation.ts gains the publish helper; soft-fails on network error
  with no user-visible warning.
* persistence.ts adds operator_api_key store; no schema bump because
  it lives in the existing STORE_DEVICE namespace.

Tests:
* 257/257 game tests pass (was 234; 23 new across routes + service).
* No em-dashes anywhere.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: Tim Thomas <0800tim@gmail.com>
… movement at 1 + view-pool link

Four tidy-ups on /leaderboard from Tim's dev screenshot pass:

1. Global / Friends / Country lived in the AppShell subHeader pill
   strip. They now sit inside the leaderboard card above the
   Humans / Bots / My Pools audience tabs, so both filter rows are
   colocated with the list they filter. AppShell loses its subHeader,
   freeing vertical space.
2. Tightened .vt-lb-page vertical gap 18px to 10px and cancelled
   DraftPreviewBanner's own 16px bottom margin inside the page so the
   yellow Preview-data pill now sits tight against the Perfect-track
   chip and the hero tiles below it. Was previously a noticeable
   half-inch of negative space.
3. MovementIndicator is capped at one position per match: any single
   match result moves a bracket by at most ±1 relative to its
   neighbours, so showing 2 / 3 / 4 was a lie about the underlying
   physics. Glyph still encodes direction; the number is always 1
   (or empty when flat). Aria label updated to match.
4. "My Pools" tab now lists the user's pools as rows with a
   gold "View pool to" link to /s/<slug>, plus a footnote linking
   to /pools and /syndicates/new. Empty state preserved as fallback.
   Mock pulls the first three MOCK_SYNDICATES; swaps to
   /api/v1/leaderboard/my-pools on Phase 1 Task 8.

Refs: docs/superpowers/specs/2026-06-07-bot-arena-design.md §5
Tim 2026-06-07.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: Tim Thomas <0800tim@gmail.com>
…ountry / My Pools

Collapsed the two-row Compare + Audience layout into one row, in the
order Tim signed off on:

  Humans   - prize-eligible humans only
  Bots     - bot race, separately ranked
  Global   - humans and bots merged
  Country  - humans filtered to the viewer's country
  My Pools - the user's own Pool memberships

Friends was dropped (no friends-graph in the database). The bracketed
hints in Tim's brief were wiring notes for me, not labels: the buttons
render just the bare words.

Behaviour per tab:
  - humans / bots / country reuse the existing <Leaderboard /> with
    the appropriate audience filter.
  - global passes scope=null so the mock returns the combined pool;
    when the API lands it will be audience=any.
  - country currently reuses the humans pool; the country narrowing
    happens server-side once the viewer's ISO code is in scope.
  - my pools renders the View pool ->/s/<slug> rows (unchanged).

Single-row strip horizontally scrolls on narrow viewports rather than
wrapping so the pill rhythm stays intact. Tests bumped to cover all
five tabs and the View-pool link target.

Tim 2026-06-07.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: Tim Thomas <0800tim@gmail.com>
…point

Hero rewrite per Tim's brief:

* Photo header in the /the-bet style: full-bleed bot-arena-hero.webp
  + .jpg, dual-gradient scrim, headline sits bottom-left. Image is
  the 2816x1536 robo-stadium PNG resampled to 1920w then quality-82
  WebP (395KB) + quality-82 JPEG (571KB) via ImageMagick. No content
  alteration; only the resample to a sensible delivery size.
* New banner heading replaces the old marketing tagline:
    Spawn millions or billions of bots.
    Change predictions anytime.
    Will one of your bots dominate the bot leaderboard?
* The previous lede paragraph ("104 matches. 9.74 x 10^43 brackets,
  every pick anchored to Bitcoin, US/bin/bash anchor cost, ...") moves
  out of the hero and into the body just above the "Start in five
  minutes" section, so the photo carries the banner cleanly.

Live stats strip (ArenaStats client island):

* Three chips sit beneath the hero, hidden until either the device
  has bots or the server-aggregate has crossed zero.
    1. My swarm     - sum of bots in this device's IndexedDB.
    2. Still perfect - identical to my swarm pre-kickoff, will drop
                       as match results land.
    3. Bots in the arena - global aggregate across all swarms.
* Polls /v1/swarm/totals every 45s; endpoint caches for 60s. New
  browser windows / new accounts see the total tick up within a
  one-minute window.

Server-side:

* SwarmClaimStore.totals() returns SUM(total_bots), COUNT(*) and
  COUNT(DISTINCT node_id) over swarm_claims in one query.
* GET /v1/swarm/totals adds an in-memory 60s cache + HTTP
  Cache-Control public/max-age=30/SWR=60 so the edge and the
  browser also de-dup.

Tim 2026-06-08.

Refs: docs/30-browser-swarm-architecture.md, /the-bet hero pattern.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: Tim Thomas <0800tim@gmail.com>
…s into dev tree

# Conflicts:
#	apps/web/app/leaderboard/page.tsx
…t Arena hero card

- Add print-styled press release PDF at /press/tournamental-press-release-2026-06-07.pdf
  (Chrome-headless rendered from a Georgia / A4 template).
- Add web-readable white paper at /whitepaper/perfect-bot-bracket/ (dark theme,
  Fraunces headings, Inter body).
- Add generator script tools/youtube-discovery/build_press_2026-06-07.py so the
  HTML + PDF can be re-rendered if the source markdown changes.

Home page restructure:
- New .vt-bots-feature image-overlay card promoting the Bot Arena: eyebrow,
  headline "Send millions of bots into battle.", body, CTAs to /bot-arena +
  the new press PDF + the white paper. Mirrors .vt-bet-feature but uses the
  /hero/bot-arena-hero asset and a cool-blue accent instead of gold so the
  two cards read as a pair without competing.
- Promote the Set picks + Run a pool CTAs out of the hero into a standalone
  2-col condensed row (.vt-home-ctapair) sitting between the new Bot Arena
  card and the existing bet feature card.
- Existing Tournamental? Maybe hero and all downstream sections unchanged.

Refs: docs/internal/press-2026-06-07-perfect-bot-bracket/
Signed-off-by: Tim Thomas <0800tim@gmail.com>
Next.js doesn't auto-resolve index.html under /public, so
/whitepaper/perfect-bot-bracket 404'd while the file was reachable
only at /whitepaper/perfect-bot-bracket/index.html. Add two rewrite
rules (no slash and trailing slash) so the clean URL serves the
static HTML.

Refs: docs/internal/press-2026-06-07-perfect-bot-bracket/
Signed-off-by: Tim Thomas <0800tim@gmail.com>
The 2-col CTA row's "Set my picks" button now flows straight into
the Step 1 explainer, then into the Tournamental? Maybe house bet,
rather than jumping over the bet card to find the explainer below.

Refs: Tim 2026-06-08 screenshot feedback
Signed-off-by: Tim Thomas <0800tim@gmail.com>
The earlier promotion of the CTAs into a standalone 2-col row pushed
them below the Bot Arena card, hurting the at-a-glance read on the
hero. Restore the original .vt-home-hero-ctas block inside the hero
so the action sits over the editorial banner exactly as prod does,
and drop the now-unused .vt-home-ctapair styles.

Section order is unchanged otherwise: hero -> Bot Arena card ->
Step 1 picks -> Tournamental? Maybe -> rest.

Refs: Tim 2026-06-08 screenshot feedback
Signed-off-by: Tim Thomas <0800tim@gmail.com>
The earlier cool-blue accent on .vt-bots-feature (eyebrow, headline
em, body em, primary CTA, scrim radial) was intended to "not fight"
the gold bet card below, but Tournamental is a single-gold brand so
the blue read as off-brand. Swap every blue token for the exact gold
values the bet card already uses (#d9b463 eyebrow, #f6c64f headline
em, #f5e1a9 body em, #fcd34d->#f59e0b CTA gradient, rgba gold scrim).

Refs: Tim 2026-06-08 screenshot feedback
Signed-off-by: Tim Thomas <0800tim@gmail.com>
…r million bots)

BREAKING CHANGE: v0.3.0 drops the bot/bot_pick/bot_score tables.
Operators upgrading from v0.2.0 MUST wipe their data volume.

Why
---
v0.2.0 stored every bot's every pick as a SQLite row -- 104 million
rows for a million-bot swarm, ~16 GB on disk + ~3 GB WAL. We
proved (apps/web/__tests__/regeneration-deterministic.test.ts)
that picks are deterministic functions of
(run_seed, bot_index, strategy), so storing them was redundant
work. v0.3.0 keeps only the recipe and recomputes picks on demand.

What changed
------------
* storage.ts schema: drops bot, bot_pick, bot_score. Adds
  swarm_run (run_seed, strategy, total_bots, per_match_roots_json,
  ...) and match_score_summary (per-swarm aggregate after each
  scored match). Meta + commit_log + match_result kept verbatim.
* generator.ts: writes ONE swarm_run row per (seed, strategy).
  Computes per-match merkle root over compact <base36><h|d|a>
  leaves on the fly and stores the root in
  swarm_run.per_match_roots_json. Adds regenerateBotPickForMatch()
  and leafForBotPick() exports.
* scorer.ts: iterates [0, total_bots) and regenerates each bot's
  pick via the formula instead of SELECT FROM bot_pick. Per-swarm
  match_score_summary row written per scored match. O(bots *
  settled_matches) regenerations, ~3 micro-seconds each.
* scheduler.ts: commitMatch reads per-match roots directly from
  swarm_run instead of merkle-hashing fresh from bot_pick. O(1)
  per match regardless of bot count. pickLeaf removed.
* server.ts: /v1/proof/:match_id/:bot_id removed (proof is now
  client-side regeneration). /stats adds swarm_breakdown.

Storage delta
-------------
        v0.2.0       v0.3.0
  1K     16 MB        2 KB
  10K   160 MB        2 KB
  100K  1.6 GB        2 KB
  1M     16 GB        2 KB
  1B     16 TB       ~50 KB

(All v0.3.0 sizes are dominated by the metadata + per-match merkle
roots, not by bot count. The bots themselves live as integers in
[0, total_bots).)

Migration
---------
1. Stop the v0.2.0 container: docker compose down
2. Wipe the data volume: docker volume rm billion_bot_data
3. Update the consumer's tarball reference to 0.3.0
4. Rebuild + start: docker compose build --no-cache && docker compose up -d
5. Re-run generate from scratch; the swarm runs from index 0

Done in this commit
-------------------
* packages/bot-node/ rewritten + tested (17/17 pass)
* apps/billion-bot/{package.json,Dockerfile} updated to 0.3.0
* CLAUDE.md gains 'SDK release process' section covering the
  bump-test-pack-smoke-publish flow for future agents

Not yet done (Tim must do)
--------------------------
npm publish: my token returned 404 on @tournamental/bot-node so
the v0.3.0 tarball is only available locally. Run
`cd packages/bot-node && npm publish --access public` from a
shell with a token that has @tournamental/* write scope, otherwise
external users still get v0.1.0.

Refs: docs/30-browser-swarm-architecture.md (matching contract)
Tim 2026-06-08.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: Tim Thomas <0800tim@gmail.com>
… + leaderboard fold

Recalibrate the browser-swarm + bot-node chalk strategy so group matches
favour the stronger side instead of collapsing onto Draw, and restrict
darling-team picks to the FIFA top 16 so cup winners stop surfacing
longshots. Wire live Polymarket odds (game-service /v1/odds/*) into the
/run page with a FIFA-rank fallback. Add an incognito-browser gate on
/run that blocks Start until the operator acknowledges the no-persistence
risk or enables Supabase replication. Fold swarm_summary aggregates into
/v1/swarm/totals so federated bot-node containers move the bot-arena
headline. Tighten the single-bot bracket view (drop noisy 2nd/3rd
columns) and surface a live bot count on /profile.

Refs: sessions/2026-06-08_bot-strategy-recalibration
Signed-off-by: Tim Thomas <0800tim@gmail.com>
…erated swarm container

The operator-facing Docker container that wraps @tournamental/bot-node:
a tiny HTML dashboard (Start/Stop/throttle, live throughput + CPU
readouts, aggregate-publish status), the compose file, and the bundled
104-match WC2026 fixture catalogue. Build artefacts (packed tarballs,
runtime SQLite) are gitignored.

Signed-off-by: Tim Thomas <0800tim@gmail.com>
…) signature in scoped path

The correct_picks work on main changed matchesAvailableTo to take a
recorded-kickoffs array; the Bot Arena scope branch still called the old
3-arg form. Semantic merge conflict, caught by typecheck before reaching
main.

Signed-off-by: Tim Thomas <0800tim@gmail.com>
@0800tim 0800tim merged commit b47e72a into main Jun 7, 2026
9 of 12 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant