← Sam Mozer
College basketball market tracker
Sports bettors say you should "fade the public": bet against the side most people are on. I built a pipeline to collect the data and test it.
- Role
- Solo project
- Data
- 175K+ snapshots · 7,000+ games
- Stack
- Workers · D1 · TypeScript · Python
code on GitHub soon
How it works
Cron trigger every 15 minutes
│
▼
Worker ── fetch today's games, lines, and public splits ──► Action Network API
│
├─► games one row per game, final score filled in when it ends
├─► bet_snapshots spread, moneyline, and total, with ticket % and money %
└─► low_bet_games games where one side is under 30 / 20 / 10 / 5% of bets
│ (all in D1)
▼
dashboard + JSON API ──► Python notebooks ──► backtest
- Collection. A cron-triggered Worker pulls lines and betting percentages every 15 minutes and writes a time-series snapshot to D1, so I can see how the public and the line moved before tip-off, not just the final numbers.
- Labeling. When games finish, the Worker records the result and flags low-public-bet games by threshold and bet type (spread, moneyline, total), using both ticket share and money share.
- Backfill. Python scripts imported historical games, so the dataset covers more than the time the tracker had been running.
- Serving. The same Worker serves a small dashboard (current games, overall stats, historical low-bet games) and a JSON API for the analysis notebooks.
The backtest
I ran a grid of 240 strategies over four knobs: the maximum spread, the maximum share of public tickets, the minimum "sharp money" gap between money and tickets, and the minimum number of bets. For each one I measured record, win rate, ROI, and a p-value.
The best-looking strategies came from small samples, roughly 20 to 80 games each. After testing 240 variations, some of those will look good by chance, so I treat them as hypotheses to check against a new season, not a proven edge. That was the most useful lesson of the project: it's easy to find a pattern and much harder to prove it's real.