TxLINE-powered AI prediction arena for the Superteam hackathon — a phone-first app where you predict match outcomes against an AI analyst and the market (Solo, free), plus a paid Odds Arena tournament that settles through an on-chain USDC escrow on Solana devnet.
pnpm workspace: UI, worker, adapters, scoring, DB, and the Anchor escrow program.
apps/
web/ Next.js app (design UI: Arena / Odds Arena / Board / Data)
app/api/arena/ on-chain Odds Arena routes (config/faucet/enter-tx/settle/claim-tx/status)
worker/ replay/live ingest + local state API
packages/
txline/ TxLINE client, auth helpers, SSE parser, IDL
ai/ AIAnalyst contract, mock/HTTP/borrowed-reference adapters, input hashing
scoring/ pure scoring helpers
db/ Drizzle schema, SQL migration, local persistence fallback
shared/ shared constants/types
fixtures/
recorded/ deterministic demo data + real captured fixtures
programs/
odds_arena/ Anchor escrow program (USDC tournament pool, deployed on devnet)pnpm install
pnpm check
pnpm dev:webIn another terminal:
pnpm dev:workerpnpm phase0:replay
pnpm replay:sample
pnpm replay:txline
pnpm replay:state-test
pnpm phase0:ai
pnpm ai:contract-test
pnpm scoring:test
pnpm db:check
pnpm db:roundtrip-test
pnpm db:generate
pnpm txline:fixture-test
pnpm phase0:txlineLive TxLINE commands require subscription activation first. The local check, replay, AI smoke, and TxLINE fixture test do not.
pnpm txline:guestPut the printed TXLINE_JWT into your shell or .env, run the free-tier on-chain subscribe, then set:
TXLINE_SUBSCRIBE_TX_SIG=<confirmed subscribe tx>
SOLANA_KEYPAIR=.keys/txline-devnet.json
TXLINE_SELECTED_LEAGUES=Activate the API token:
pnpm txline:activateThen smoke-test data access:
TXLINE_BASE_URL=https://txline-dev.txodds.com \
TXLINE_API_AUTH_MODE=jwt-plus-api-token \
TXLINE_COMPETITION_ID=72 \
TXLINE_ODDS_FIXTURE_ID=17588245 \
TXLINE_SCORES_FIXTURE_ID=17588309 \
pnpm phase0:txlineConfirmed on txline-dev: bearer API-token mode returns 401; use jwt-plus-api-token.
The fixture-backed regression checks the recorded devnet snapshot:
pnpm txline:fixture-testThe replay harness converts both the synthetic fixture and the recorded TxLINE
devnet snapshot into internal beat_replay_event_v1 events:
pnpm replay:sample
pnpm replay:txlineEach replay produces odds updates, score updates, and a final result event. The worker can consume the same state reducer:
WORKER_RUN_REPLAY=1 REPLAY_SPEED=100000 pnpm dev:workerThe worker also exposes a local replay state bridge:
pnpm dev:worker
curl -fsS http://127.0.0.1:8788/health
curl -fsS http://127.0.0.1:8788/state
curl -fsS -X POST 'http://127.0.0.1:8788/replay/step?count=2'
curl -fsS -X POST http://127.0.0.1:8788/replay/start
curl -fsS http://127.0.0.1:8788/events
curl -fsS http://127.0.0.1:8788/leaderboardThe bridge persists replay events and optional pick/score records through the DB package's local JSON fallback, so demo leaderboard state survives worker refreshes without requiring hosted Supabase.
Solo mode detects Phantom directly from window.solana in the browser. Free
mode never asks for transaction signing: connecting the wallet only supplies a
public key so picks can be saved under that address.
NEXT_PUBLIC_WORKER_URL=http://127.0.0.1:8788 pnpm dev:webNo-wallet recording remains supported. If Phantom is absent, the app shows a local-only state and all replay controls still work after choosing a pick.
The Arena can run on a real TxLINE World Cup fixture (real teams + real odds):
pnpm txline:capture-real # poll fixtures, write a real pre-match fixture -> fixtures/recorded/real-match.json
pnpm dev:web # Arena shows the real match; pick is persisted to .data/picks.json (survives reload)
pnpm txline:settle-real # after FT: pull the real final score -> Arena settles on real dataapps/web/app/_data/load-arena.ts prefers fixtures/recorded/real-match.json
when present (override with ARENA_FIXTURE), else the deterministic synthetic
fixture so the demo always works.
The paid tournament settles through an Anchor escrow program odds_arena
(programs/odds_arena), deployed on devnet at
GTcSyA2M8BVd27idJJw7ajhnSxrm45tkw7R9moxpAPXc. Round cycle
open_tier → enter → lock_tier → settle_entry → sweep_rake → claim: USDC into an
escrow PDA, a keeper posts per-entry payouts (bounded on-chain by
committed_payout ≤ pool, so escrow can't be over-paid), rake is swept to the
treasury, winners claim (PDA-signed). A stuck round is recoverable via
refund_entry (admin, or anyone 24 h after lock). Rake-only, no house position;
TxL is never used for money.
Build + run the full round on a local validator:
pnpm anchor:build
solana-test-validator --reset \
--bpf-program GTcSyA2M8BVd27idJJw7ajhnSxrm45tkw7R9moxpAPXc \
programs/odds_arena/target/deploy/odds_arena.so
RPC_URL=http://127.0.0.1:8899 pnpm anchor:roundSame round on devnet (funds ephemeral keypairs from a wallet, prints explorer links):
RPC_URL=https://api.devnet.solana.com FUNDER_KEYPAIR=.keys/txline-devnet.json pnpm anchor:roundThe Odds tab has a "Live on devnet" panel: connect Phantom (devnet), get
test-USDC, stake via a Phantom-signed enter, settle, claim — real on-chain
txs. The server builds the transactions (Anchor + IDL); the browser only
signs+sends (window.solana); the operator key stays server-side.
node scripts/devnet-arena-setup.mjs # create a test mint + open a round -> .data/devnet-arena.json
pnpm dev:web # Odds tab -> "Live on devnet": Connect -> Get test USDC -> Enter -> Settle -> ClaimStaking commits the canonical slip_hash (borsh EntryCommit, VRE-parity) and
locks the slip once staked. Routes: player /api/arena/{config,faucet,enter-tx, express,claim-tx,status}; operator (CRON_SECRET-gated) /api/arena/{settle,sweep, rotate}. The demo round is single-entry-per-wallet; rotate?force=1 opens a fresh one.
Hardening smoke: node scripts/anchor-hardening-smoke.mjs (payout-bound, sweep, refund).
Verifiable scoring (VRE): committed slips + proven TxLINE facts are scored by VRE's
public POST /api/conditions/score handle — verifiable S + ranking + fc7 artifact,
byte-for-byte identical to our local scoreExpress. POST /api/arena/verify-round
surfaces the MATCH; settlement attaches the artifact hash. Payout is allocated locally
(deterministic); VRE is the independent proof layer (it never custodies funds).
The DB package contains a Supabase Postgres-ready Drizzle schema plus a checked SQL migration:
pnpm db:check
pnpm db:generate
pnpm db:roundtrip-testTables covered: fixtures, market_snapshots, ai_calls, users, picks,
scores, and replay_events. Local demo persistence uses
BEAT_LOCAL_DB_PATH and defaults to .data/beat-local-db.json; .data/ is
ignored. Hosted Supabase wiring is deferred until DATABASE_URL is available.
The app depends on a stable AIAnalyst adapter contract (packages/ai), not bot
internals. Pick the adapter with AI_PROVIDER:
mock(default) — deterministic, secret-free.borrowed-reference— analysis-derived probabilities / reasoning / risk flags from the borrowed bot's pre-match pipeline; no key, no network.http— call an external endpoint returningprobabilities.home/draw/away,confidence,pick,reasoning, optionalsources/latencyMs/costUsd/model.
Every call is tied to a canonical hashed pre-match input
(packages/ai/src/input.mjs), so the read is reproducible and verifiable —
provider + inputHash are surfaced on the Data screen. Invalid probabilities or
picks fail fast. The borrowed repo itself stays quarantined (.quarantine/,
gitignored) and is never committed or imported wholesale.
pnpm phase0:ai
pnpm ai:contract-testSolo scoring is pure and offline:
pnpm scoring:testThe scoring package resolves 1X2 from TxLINE soccer stats (Stats[1] /
Stats[2]), computes AI-market divergence for home/draw/away, derives the
market favorite from probabilities, and returns deterministic user points with a
breakdown for correct pick, beat-AI, beat-market, and underdog bonus.