One sentence: estimate how many goals each side “should” score, then spread those goals over every possible final score — and every market (winner, totals, both teams to score, spreads) is just an area on that grid. It is the classic textbook soccer model, kept deliberately simple because the backtests showed even this loses to the market.
Results in → attack & defense strengths
From each league's past results (ESPN, keyless): every team gets an attack strength (goals it scores vs average) and a defense strength (goals it concedes vs average), regularized toward the league mean so a team with 3 games isn't declared a superpower. (fitPoisson, src/server/arena/poisson.ts)
Strengths → expected goals (λ)
For a fixture: home expected goals = league average × home attack × away defense (plus home advantage); away likewise. Two numbers, e.g. 1.5 and 1.1 — that's the model's entire opinion of the match.
λ → the score matrix
The Poisson distribution answers 'if a team should score 1.5, how often does it score exactly 0, 1, 2…?'. Doing that for both sides independently gives a probability for EVERY final score — the grid below.
The matrix → every market
P(home win) = sum of cells below the diagonal. P(draw) = the diagonal. Over 2.5 goals = cells where the total ≥ 3. Both teams to score = cells where neither side is 0. One grid prices everything a fixture lists — that's why the mapper lives in exactly one file.
Point-in-time honesty
In every backtest, strengths are fit ONLY on games before the fixture being predicted — the model never peeks. Verdict: Polymarket out-prices it everywhere tested, sharp leagues and obscure ones alike (arena page, RESULTS.md).
Computed live with the model's own scoreMatrix() at λhome = 1.5, λaway = 1.1 (a typical fixture with a modest home favorite). Darker = more likely; the likeliest scores are labeled.
Summing the regions of this exact grid: home win 46.4% (below the diagonal), draw 25.8% (the diagonal), away win 27.8%. A moneyline forecast is nothing more mysterious than that addition.
src/server/arena/poisson.ts — one file, unit-tested, imported by every script that forecasts, settles or backtests soccer. A change here changes all of them at once, which is the design.DEFAULT_PRIOR = 4: how many league-average phantom games anchor each team's strengths. Raise it and small samples matter less.This site writes probabilities as cents per $1: a 60c price means the market thinks something is 60% likely, and a correct $0.60 bet pays $1. If your true chance is better than the price, the bet is good; if worse, it's bad. That one sentence is most of sports betting.
A bet on which team wins the game, nothing else. American odds like -150 mean 'bet $150 to win $100' (a favorite); +130 means 'bet $100 to win $130' (an underdog). Both convert to an implied probability.
A sportsbook's two sides always add to MORE than 100% — the extra is the vig, the book's built-in fee. De-vigging scales both sides down so they add to exactly 100%; the result is the book's honest opinion, called the fair or no-vig price.
A feature is one pre-game fact turned into a number (a rating gap, a yes/no flag). A weight is the knob saying how much that fact matters, learned from past games. The model is nothing but features × weights, summed, squashed. Adding a feature = adding a column; training finds its knob.
The simplest honest prediction machine: multiply each input by a learned weight, add them up, and squash the total through an S-shaped curve so the answer is always between 0 and 1 — a probability. The weights are learned from past games by nudging them until predictions fit outcomes.
The S-shaped curve that turns any sum into a probability. Sum 0 → 50%. Sum +2 → about 88%. It never quite reaches 0% or 100% — the model is structurally incapable of certainty, which is a feature.
How the knobs get set: predict every past game, measure how wrong you were, nudge every weight a tiny step in the direction that reduces the wrongness, repeat thousands of times. No insight, no magic — just accumulated nudging. Everything interesting lives in the features you feed it.
Do the percentages mean what they say? Collect every game the model called ~65% and check that ~65% actually went that way. A model can be calibrated and still lose to the market — calibration is honesty, not edge.
Writing down what you expect BEFORE running the test. Try enough features and something will look great by pure luck; the registered prediction plus a fixed bar (|t| ≥ 2, stable weights) is the defense. This project's ledger: two nulls (MLB form, MLB head-to-head) and one partial pass (NFL QB availability).
Before the weights see an input, it is rescaled to 'how unusual is this, in standard deviations from average?'. That puts ERA (runs) and WHIP (runners) on the same footing so their weights are comparable.
The honesty meter for probability forecasts: the average of (prediction − outcome)², where the outcome is 1 or 0. Lower is better. Always guessing 50% scores 0.25 — a model has to beat that to know anything.
A 0.5-to-1 score for 'does this single number sort winners above losers?'. 0.5 = coin flip, 1.0 = perfect. A feature with AUC 0.66 alone is genuinely informative; 0.52 is barely a whisper.
How many standard errors an effect is away from zero. Roughly: |t| ≥ 2 means the effect is probably real; below that it is indistinguishable from luck. This site refuses to believe anything under t = 2, and distrusts single t ≥ 2 results too (see the arena page).
Did the price move toward your side after you bet? The closing price is the market's most informed opinion, so consistently beating it is the fastest reliable evidence of skill — much faster than waiting for wins and losses to pile up.
The stake-sizing formula that maximizes long-run growth: bet a fraction of bankroll proportional to your edge. Everyone who uses it bets a QUARTER of what it says, because overestimating your edge is easy and overbetting is ruinous.
Tested on games the model never saw while learning. A model graded on its own training data is a student grading their own exam — every honest number on this site is out-of-sample, and it's why our numbers look worse (and truer) than most.