predball fields Tommy, a bot that competes in the SportsPredict ProbabilityCup: before each match the platform publishes ~15–20 yes/no questions ("Will Spain win?", "Will there be 10+ corners?", "Will Messi score or assist?"). Tommy reads the questions, prices every one from a single generative model of the match, and submits a probability through the official bot API — fully unattended, never abstaining.
It just finished its first full tournament: the 2026 World Cup edition, group stage through the final.
| Final rank | 268 / 4,011 — top 6.7% |
| Final score | 1,702 RBP |
| Competitors outperformed | 3,743 (93.3% of the field) |
| #1 of all Montenegro 🇲🇪 entrants | (national leaderboard) |
| Questions graded | 455, across 30 matches |
| Directional hit rate | 58.5% — picked the more-likely side 3 times in 5 |
| Brier skill vs coin-flip | +0.086 (eliminated ~9% of baseline error) |
Best performances
| Matchday | Round | RBP gained | Running total → rank |
|---|---|---|---|
| Jul 14 | Semi-finals | +267.7 🥇 | 1,265 → #303 |
| Jul 18 | Semis settle + slates | +263.1 🥈 | 1,528 → #273 |
| Jul 19 | The Final (all-in) | +173.2 | 1,702 → #268 |
| Jul 9 | Quarter-finals | +148.4 | 969 → #342 |
| Jul 7 | Quarter-finals | +140.5 | 821 → #376 |
Tommy climbed from a low of rank ~940 mid-tournament to 268 by the final — the edge compounded as the knockout multipliers (×2, ×3) rewarded being sharper than the field exactly when it mattered most.
vs. the crowd. RBP is scored relative to the field average —
RBP = (field-average Brier − your Brier) × multiplier × 100. A positive total is,
by construction, proof of being sharper than the average competitor; Tommy finished
+1,702 while the median player finished at −11 and fewer than half the field
(46%) finished positive at all. The edge came from systematically fading markets the
crowd reliably over-bid — star scorers, penalties, "a goal before the hydration break"
— where Tommy's calibration was near-perfect (in the 10–50% band, predicted rate
0.22–0.41 vs. realised 0.22–0.42).
The ProbabilityCup is a probability-forecasting contest, not a betting game. For every
match you answer a fixed slate of yes/no questions with a probability in [1, 99], and
you are scored on the Brier score (squared error of your probability vs. the 0/1
outcome) relative to the whole field's average. You gain points only by being more
accurate than the crowd; knockout matches count double, the final triple. With ~4,000
competitors and a purely relative metric, the game is won by consistent, well-calibrated
edges — not the occasional bold call.
Two halves separated by a hard brain/app firewall: footy/ is pure mathematics and
does no I/O (no HTTP, DB, or files); probcup/ does all the talking to the outside
world. They meet at one seam (footy/contracts), which keeps every probability
reproducible and independently testable. Every question carries two numbers —
p_honest (the calibrated, truthful belief that models train against) and p_submit
(what's actually sent, after an optional rank-chasing tilt) — so leaderboard tactics
never contaminate the honest model.
flowchart LR
SP[("SportsPredict<br/>bot API")]
ODDS[("The Odds API<br/>24 bookmakers")]
subgraph APP["probcup/ — the APP (plumbing + I/O)"]
direction TB
COL["collectors<br/>odds · questions · crowd"] --> WH[("warehouse<br/>bitemporal Postgres")]
WH --> PROV["provider<br/>assemble_inputs as-of"]
PUB["publisher<br/>outbox + dry-run/live submit"]
ORCH["orchestration<br/>scheduler · passes · archive"]
end
subgraph BRAIN["footy/ — the BRAIN (pure maths, no I/O)"]
direction TB
MODELS["models M0–M5<br/>de-vig · goals · players"] --> DERIV["derivation<br/>question to probability"]
DERIV --> CALIB["calibration<br/>honest belief p_honest"]
CALIB --> OVER["overlay<br/>strategic tilt to p_submit"]
end
ODDS --> COL
SP --> COL
PROV -->|FeatureInputs| MODELS
OVER -->|p_submit| PUB
PUB --> SP
ORCH -.drives.-> PROV
ORCH -.drives.-> PUB
classDef app fill:#e8f0fe,stroke:#4285f4;
classDef brain fill:#fce8e6,stroke:#ea4335;
class COL,WH,PROV,PUB,ORCH app;
class MODELS,DERIV,CALIB,OVER brain;
- collectors → warehouse ingest odds, question slates, and post-match crowd/results
into a bitemporal store (everything stamped
observed_at, so backtests can't peek ahead). - provider assembles an as-of-correct snapshot of inputs for a given match + time.
- models → derivation build one match model and read every question off it.
- calibration → overlay turn raw model output into the honest belief, then into the submitted number; orchestration schedules the passes and the post-match archive.
One generative stack produces a single joint distribution of the match; every question is just a functional of it (directive: output full distributions).
team strengths (Elo / Poisson-GLM)
│ λ_home, λ_away
▼
Dixon-Coles score grid P(i,j) = Poisson(λ_h)·Poisson(λ_a)·τ(i,j) ← goals/result
│ + M2 negative-binomial count models ← shots/corners/cards
▼
one seeded simulator (N=20k joint draws) ← comparatives, halves, first goal, player props
│
▼ p_honest = isotonic_calibrate( P(question) )
▼ p_submit = clip( p_honest + k·(p_honest − ĉ) ), k gated by a logged γ-schedule
- Dixon-Coles models correlated home/away goals with a low-score correction
τ; strengths come from bookmaker-odds inversion live, and an odds-free Poisson-GLM fit on a 49k-match corpus for backtests. - Calibration is per-family isotonic regression, shrunk toward identity while data is thin, so probabilities match observed frequencies.
- Overlay is the strategic shell: an M5 crowd model estimates where the field will land (ĉ), and Tommy presses its edge away from the crowd by a magnitude the γ-schedule sets from the rank deficit — bounded so the honest call's direction is never flipped.
docs/PROJECT-GUIDE.md— the full guide: setup,.env, every command, the ladder of how each question type is answered, phase history.docs/ARCHITECTURE.md— the why ·CLAUDE.md— the binding engineering contract.- Per-phase runbooks:
P0·P1·P2·P3·P4·P5.
uv sync && uv run python scripts/devdb.py && uv run alembic upgrade head # setup
uv run python -m probcup.orchestration.run --match <id> # dry-run a match
uv run pytest footy/tests/coherence -q # the merge gateSubmissions stay dry-run unless both
PROBCUP_LIVE=1and--liveare set — a deliberate double safety catch.