MLB Playoff Odds Tracker

Try the lite (Tailwind) version →

Written each morning

Games

Slate and scores from ESPN. The right-hand number on each line is that team's current make-playoffs odds, so you can see what the game is worth.

Ask

Answers come from the numbers on this page and nothing else — no scores, rosters or history we do not log, and no scenarios: the odds we store are a single number per team, not a distribution.

Odds over time

Latest snapshot

How magic numbers work

Every team has one — the division path for whoever currently leads their division, the wild-card path for everyone else. It is the combined wins-for-them and losses-for-a-rival still needed to clinch that path, so it shrinks as both sides keep playing.

It only shows up in the table once it is within games remaining. A magic number of 41 with 31 games left isn’t information, it’s just a large integer — so most teams show a blank for most of the season, including plenty that go on to make the playoffs. That’s the answer to “why do some playoff teams not have one”: they do, ESPN just hasn’t made it small enough to matter yet.

Clinched means the number already reached zero. ESPN keeps counting past that instead of stopping, so a team can be locked into its path (division or a wild-card berth) days before the number disappears from a standings page.

About this project

A daily log of MLB playoff odds from two independent models, plus each morning’s games and a short written recap for every team. It exists because playoff odds are published as a snapshot — every source shows you today and forgets yesterday. Recording them daily turns a number into a trend, which is the part that actually tells you something.

cron 6am → freshness check → scrape 2 sources → Postgres → Claude: recap + preview → static page

Deterministic spine, model at the edge

Fetching, parsing, storing and verifying are plain Python — cheaper, reproducible, and when they break you get a stack trace instead of a confident wrong answer. Claude is called at two points, both writing prose from numbers the scrapers already verified: a recap of yesterday, and a preview of tonight that may search the web for news no table of ours contains. Both are instructed never to invent a statistic, and the preview is told to trust our figures over anything it finds. If a model is deciding what the data is, the design is wrong. It should only decide how to say it.

What it cost

Thirty summaries a day is roughly 10k input and 5k output tokens — cents. The interesting constraint was never price. It was making sure the numbers handed to the model were right in the first place.

What was actually hard

  • Knowing what to keep. A table holding current state is not a log. One source served only current standings, so every run silently destroyed the previous day — unrecoverable. That decision has to be made before collection starts, not after.
  • Time zones. Timestamps arrive in UTC; the sport runs on Eastern. An 8pm game is already tomorrow in UTC, which quietly filed results under the wrong day and would have drawn the two models a day apart.
  • Two models disagreeing is the signal. They see identical W-L records, so a 25-point gap isn’t noise — it’s one method reading recent results and the other reading roster talent.
  • Nothing tells you when it breaks. Exit codes vanish under cron, so the job checks its own freshness and raises an alert rather than failing silently into a gap in the chart.
  • The host dictates the store. This ran on SQLite until a possible move to Heroku, whose dynos have an ephemeral filesystem — reset on every deploy and daily restart. A SQLite file would vanish within a day, so the whole thing moved to Postgres.

Reusing the pattern

The shape — schedule, collect, store durably, verify, then use a model for the one step that needs judgment — is not specific to baseball. Swap the scrapers and the prompt and it is a bot for prices, listings, deploys, or an inbox. The build notes and the traps worth knowing are in ARCHITECTURE.md, and every prompt and setting actually used is visible on the what we send to Claude page. The design tokens and components every page shares are catalogued on the styles page, and every page and endpoint this app answers is indexed on the routes page.