This page is the complete technical description of how our predictions are produced and how they performed in testing. It includes the results that argue against paying us for picks. That is deliberate. It is also the file we would hand to a regulator, a payment processor, or a journalist who asked us to substantiate anything on this site.
Everything on this page is reproducible from the public ledger and the open source code. If a number here cannot be regenerated from published data, treat it as an error and tell us.
We predict NFL game outcomes with an Elo rating system that uses a damped margin-of-victory update, augmented with opponent-aware expected-points-added form and rest, then convert those ratings into probabilities and correct them with isotonic regression refit each season on prior seasons only. Across 2,671 out-of-sample games from 2016 through 2025, the published model recorded a Brier score of 0.22148 against the de-vigged closing market's 0.21038, straight-up accuracy of 63.72% against the market's 66.64%, and an against-the-spread record of 1291-1317-63, or 49.50% of decided games, against a breakeven of 52.38% at standard -110 pricing. The model does not beat the closing market. Our probabilities are reasonably calibrated, which is a different and smaller claim, and it is the only claim we make.
We claim exactly two things:
We do not claim, and have never claimed, that following our predictions is profitable. Our own testing says it is not. We do not publish a win rate as a selling point, a return on investment figure, or a units-won figure. We do not accept wagers, hold funds, or pay out prizes.
Every team carries a single rating. Ratings start at 1500 and move only when a game settles.
The probability that the home team wins is the standard Elo logistic:
p_home = 1 / (1 + 10 ^ (-(R_home - R_away + HFA + rest_bonus) / 400))
After the game settles, both ratings move by the same amount in opposite directions:
delta = K * mov_multiplier * (actual - p_home)
mov_multiplier = ln(|margin| + 1) * (2.2 / (0.001 * winner_elo_diff + 2.2))
The margin-of-victory multiplier is the standard log-damped form. It gives more
credit for a 24-point win than a 3-point win without letting a single blowout
dominate a team's rating, and the winner_elo_diff denominator corrects for the
fact that better teams are mechanically more likely to win by a lot. Without
that correction the system over-rates favourites in a self-reinforcing loop.
| parameter | value | meaning |
|---|---|---|
k |
20.0 | rating points moved per unit of surprise |
home_advantage |
48.0 Elo | roughly 2 points of spread |
season_carryover |
0.75 | fraction of a rating carried into the next season |
base_rating |
1500.0 | starting and mean-reversion target |
elo_per_point |
25.0 | Elo points per point of scoring margin |
rest_per_day |
1.5 Elo | bonus per extra day of rest versus the opponent |
These values are conventional, were not tuned against the test period, and are published so that anyone can reproduce our ratings exactly. The model is deliberately simple. Its job is not to be clever; its job is to be a fully explainable baseline that we can publish, grade in public, and improve on in the open.
Raw Elo probabilities are mildly overconfident in the middle bands. We correct them with isotonic regression, which is a monotone step-fit that reshapes the probability curve without assuming that curve is logistic.
The calibrator is fitted only on seasons strictly before the season being predicted, and it is refitted every year. Fitting a calibrator on the same games you then score with it manufactures a perfect-looking reliability curve that means nothing. Seasons before we have 500 prior games available pass through uncalibrated rather than being calibrated on thin data.
The production model versions are elo+epa-v1+iso (independent) and
elo+epa+market-v1+iso (consensus).
Ratings are built strictly forward in time. For every game in the historical record the model produces a prediction using only ratings derived from games that had already finished, and only afterwards is the result used to update the ratings. There is no point at which the model sees a future game.
The market comparison uses de-vigged closing prices. Both sides' implied probabilities are divided by their sum to remove the bookmaker margin. Comparing a model to a vigged line is trivially easy and meaningless, because the vig guarantees the raw line is a biased probability estimate.
Every figure below is regenerated by a single command:
python scripts/published_figures.py
If a number on this site cannot be produced by that command, it should not be on this site. We publish two evaluations of the same models rather than choosing the flattering one.
Larger sample, weaker provenance. nflverse's spread_line is an undocumented
periodic snapshot rather than a documented close.
| model | n | Brier | ECE | ATS record | ATS% |
|---|---|---|---|---|---|
| Elo baseline | 2671 | 0.22217 | 0.02698 | 1287-1321-63 | 0.4935 |
| Independent (ours) | 2671 | 0.22148 | 0.03122 | 1291-1317-63 | 0.4950 |
| Consensus (+market) | 2671 | 0.21455 | 0.03343 | 1300-1308-63 | 0.4985 |
| Closing market | 2671 | 0.21038 | 0.01913 | 1327-1281-63 | 0.5088 |
Smaller sample, far better provenance: the median across books, captured 5–28 minutes before each kickoff, from odds we paid for and hold ourselves.
| model | n | Brier | ECE | ATS record | ATS% |
|---|---|---|---|---|---|
| Elo baseline | 854 | 0.22246 | 0.03265 | 402-431-21 | 0.4826 |
| Independent (ours) | 854 | 0.22164 | 0.03269 | 406-427-21 | 0.4874 |
| Consensus (+market) | 854 | 0.21399 | 0.04231 | 412-421-21 | 0.4946 |
| Closing market | 854 | 0.21061 | 0.02396 | 406-427-21 | 0.4874 |
At −110 on both sides a bettor needs 52.38% against the spread to break even. No model in either table clears it. Neither does the closing market against its own number — which is the sanity check that the test is well-formed rather than flattering us.
32.9% of spreads differ between nflverse and the real consensus close, but the typical difference is small: mean 0.217 points, median 0.0, and only 5.5% differ by a full point or more. Graded on the same games, the Elo baseline scores 0.4850 against nflverse lines and 0.4826 against real closes — a 0.24 percentage point gap. The weaker source was precise enough for the conclusion and not precise enough to publish closing-line value from, which is why we bought the better one.
Calibration asks a narrower question than profitability: when we say 70%, does it happen about 70% of the time? A model can be well calibrated and still unprofitable, which is precisely our situation.
Expected calibration error (ECE) is the sample-weighted mean absolute gap between predicted and observed frequency across ten probability bands. Lower is better.
| model | ECE | Brier |
|---|---|---|
| Elo baseline | 0.02698 | 0.22217 |
| Elo + EPA + rest, isotonic (published) | 0.03122 | 0.22148 |
| de-vigged market | 0.01913 | 0.21038 |
Two honest notes on that table. First, adding EPA form and rest improves the Brier score over the Elo baseline but slightly worsens measured ECE. Extra features buy sharpness at a small cost in reliability, and we publish both numbers because reporting only the one that improved would be the same selective disclosure we are criticising. Second, the market is better calibrated than we are — on this test, meaningfully so.
| predicted band | n | mean predicted | actual frequency | gap |
|---|---|---|---|---|
| 0.1-0.2 | 26 | 16.70% | 26.92% | -10.22 pts |
| 0.2-0.3 | 152 | 26.16% | 28.95% | -2.79 pts |
| 0.3-0.4 | 270 | 36.02% | 33.70% | +2.32 pts |
| 0.4-0.5 | 419 | 45.37% | 41.29% | +4.08 pts |
| 0.5-0.6 | 569 | 54.99% | 50.26% | +4.73 pts |
| 0.6-0.7 | 593 | 64.97% | 62.39% | +2.58 pts |
| 0.7-0.8 | 440 | 74.86% | 72.95% | +1.91 pts |
| 0.8-0.9 | 190 | 83.93% | 85.26% | -1.33 pts |
| 0.9-1.0 | 12 | 91.13% | 91.67% | -0.54 pts |
A positive gap means we were overconfident: we predicted the event more often than it happened.
Read the extremes with care. The 0.1-0.2 and 0.9-1.0 bands hold 26 and 12 games respectively; at those sample sizes a handful of results moves the observed frequency by tens of points and the gap is mostly noise. The bands that carry real weight are 0.3 through 0.8, which hold 2,291 of the 2,671 games, and in those bands the model runs overconfident by between 1.9 and 4.7 percentage points.
We do not publish any probability above 0.85, regardless of what the model outputs.
The honest reason is sample size. In the current out-of-sample record the 0.9-1.0 band holds twelve games — far too few to demonstrate that the model deserves that much confidence. An earlier, larger evaluation of the raw Elo baseline showed the opposite problem: roughly eight points of overconfidence at the top of the range. Between a band too thin to trust and a history of overconfidence exactly where the industry sells its "locks", the conservative policy is a hard cap, and we keep it as a design rule rather than a measured finding. If the extreme bands ever accumulate enough games to be measured properly, we will publish that table and revisit the cap in the open.
Data leakage - training on information that would not have been available before kickoff - is the most common way a sports model backtests beautifully and then loses live. It is also the easiest way to build a fraudulent-looking track record without meaning to.
The following columns are present in our source data and are banned from feature construction, enforced by an assertion in the NFL adapter that fails the build rather than warning:
| banned column | why |
|---|---|
temp |
measured at or after the game, not a pre-game forecast |
wind |
same |
home_score, away_score |
the outcome |
result |
the outcome |
total |
the outcome |
overtime |
the outcome |
Betting-line columns require a separate control. In our source dataset,
spread_line and the related price columns are overwritten in place as the
market moves. A row read today shows the current number, not the number that
existed when the game was scheduled. We therefore treat those columns as a
closing line only for games already marked final, and we never treat them as an
opening line.
Because of that, we have not published a closing-line-value figure and will not publish one until we have validated our line history against an independent source with explicit open and close objects. CLV is the metric most often quoted by services in this category and it is the metric most easily faked by reading a mutable field. Ours will be published when it is defensible and not before.
The features actually used by the production models are: team identity, prior Elo ratings derived only from earlier games, opponent-aware expected-points- added form (also computed only from earlier games), home or neutral site, days of rest for each team, and season boundaries for the carryover regression. The consensus model additionally uses the de-vigged market probability. That is the complete list.
The problem with every published pick record on the internet is that it is self-reported and editable. A losing pick can quietly vanish. A winning pick can be added afterwards. "We publish everything" is an unfalsifiable claim.
We make our record falsifiable with a commit-reveal scheme.
leaf = SHA-256(0x00 || canonical_json)parent = SHA-256(0x01 || left || right), working up the tree until one node
remains. If a level has an odd number of nodes, the last node is duplicated.
The distinct 0x00 and 0x01 prefixes are domain separation: they make it
impossible to pass an internal node off as a leaf, which is the standard
second-preimage attack on naive Merkle trees.Anyone can then recompute the tree from the revealed predictions and confirm it
produces the root we published before kickoff. If we had altered, removed,
reordered, or back-dated a single prediction, the recomputed root would not
match. The algorithm identifier recorded in every commitment file is
sha256-merkle-v1.
The commit function refuses to seal a slate after its first kickoff. A commitment created after games have started proves nothing, so we made it impossible to create one by accident.
Because it is a Merkle tree and not a flat hash, a single prediction can be proven to belong to a committed slate without revealing the rest of the slate. For our 16-game NFL Week 1 slate, an inclusion proof is four hashes long. This matters for the paid tier: we can prove a subscriber-only prediction was part of the pre-kickoff commitment without publishing the whole slate to non-subscribers.
Step-by-step instructions for verifying all of this yourself, including a 30-line script that does not use any of our code, are on the verification page.
It proves the record is complete and unedited. Every prediction we made is in it, in the form we made it, timestamped before kickoff.
It does not prove the predictions are good. Cryptography cannot make a model accurate. It only makes our reporting of that model honest, which is a different and much rarer property in this industry.
We would rather state these than have them found.
These are the changes we expect to make, published in advance so the record shows what changed and when:
Any change to the model produces a new model_version string, and every
prediction in the ledger records the version that produced it. A prediction can
always be traced to the exact model that made it.
This site publishes predictions for analysis and entertainment. We do not accept wagers, hold funds, or pay prizes. We are not affiliated with the NFL, any league, team, or sportsbook. Full disclaimers, including responsible-gambling resources, are at /disclaimers.
Last updated 2026-08-06. Model versions elo+epa-v1+iso (independent) and elo+epa+market-v1+iso (consensus).