Back in August 2023 I wrote a Python script that guessed NFL spreads. It was ugly and it worked on exactly one machine. Today that same side project has three sports, a web app, an iOS app, and a scoreboard that grades its own homework.

I want to walk through how it got here, mostly through the things I got wrong along the way. This is process, not picks.

Timeline of the project: Version 0 in August 2023, Version 1 in 2025, Version 2 in September 2026, and today’s Final Call in October 2026.

Version 0: a spider and a regression

The first version was a Scrapy spider pointed at Pro Football Reference. It pulled week-by-week game data into a CSV. Then a scikit-learn regression (I tried TPOT too, because auto ML sounded like cheating in a good way) read the CSV and printed spreads for the upcoming week.

That was the whole thing: scrape the data, fit the model, print the spreads.

Version 0 diagram: a Scrapy spider feeds a CSV file, a scikit-learn regression reads it, and it prints spreads for next week.

It taught me a lot, mostly about how fragile scraping is. A site changes a table and your model quietly trains on garbage. I also had zero way to say whether the numbers it printed were any good.

Version 1: make it a real app

In 2025 I came back to it with a bigger plan. I wanted users, a dashboard, odds from an API, and a clean way to ship it. So I split it into services: auth, data, predictions. Docker, FastAPI, CI, the whole grown-up checklist. I leaned on Copilot agents for a lot of the grunt work, and the commit log from those weeks reads like a stack of “Initial plan” followed by “Implement comprehensive…”.

Version 1 diagram: a web UI talks to auth, data and predictions services running in Docker, with CI around them and an odds API feeding in.

It was a real milestone. Predictions showed up on a real page, from real data, end to end. I was proud of a model that picked winners around 61% of the time.

Then I remembered that favorites win about two thirds of the time on their own.

The number that fooled me

As an engineer, I should have known better. A number without a baseline doesn’t tell you much.

In betting, the baseline is the closing line. By the time a game kicks off, the market has soaked up every injury, every bettor, every sharp. If your model can’t beat the closing line, it doesn’t have an edge, no matter how good its record looks.

So the question changed. It stopped being “is the model accurate?” and became “would I have gotten a better price than the market ended up with, and could I have known that at the time?”

Version 2: remember what you knew

That second question forced a rewrite. In late September 2026 I started nflcore from scratch around one rule: reconstruct what was known at any moment.

Every fact is stamped with the time it became knowable, and nothing is ever overwritten. Odds, injuries, weather, quarterback news, all of it is a dated observation. When a model makes a call, it only gets to see what existed at that cutoff. That makes look-ahead bugs very hard to write, and it makes every result something I can actually trust.

Measured against the closing line, my spread model did not have an edge. The README says so at the top. I would rather publish that than a pretty backtest.

Where it is now

The setup today is a champion and challengers system I call Final Call. One champion makes the official pick for each game and prop. A handful of challengers (margin-based, weather, market-implied, and so on) make their own calls on the same games. Everyone gets graded on closing line value, with the rules written down before the results come in. A pick freezes an hour before kickoff, so I can’t quietly edit history. You can see the NFL side of it on Lines, my public scoreboard.

Today’s architecture: observations feed a cutoff view, the Final Call champion and challengers make picks, picks freeze an hour before kickoff and are graded on closing line value, then show up on web, iOS and Discord.

A few things came along with it:

  • Three sports. NFL, college football, and NBA all run through the same machinery.
  • Web and iOS. The same screens, built from the same documents, in a browser and in a native app.
  • Discord. A place for receipts and alerts without me hand-posting anything.
  • A pre-registered ledger. If I want to test an idea, the spec goes in the log first. If I change the idea, it gets a new ID. No re-tuning after I’ve seen the data.
  • One Windows PC. It runs the scheduler and the web app. Merging to main deploys it, which keeps me careful.

The Board screen on a sample slate, with NFL, CFB and NBA tabs and a line at the top that reads: No lane has beaten the closing line yet.

The Board, on a sample slate. That line at the top is the whole point of the project.

Most of the code now gets written by Claude agents working off GitHub issues, one small piece at a time, with me reviewing.

What I’d tell 2023 me

  1. Pick the baseline before you pick the model.
  2. Timestamp everything, and never overwrite.
  3. Write down how you’ll judge an idea before you test it.
  4. Let challengers fight the champion. Don’t fall in love with a model.
  5. Be okay with “no edge yet.” It’s still a result.

The early signal so far is that being early on a price matters more than being clever. That surprised me.

Have you ever built something that looked great until you compared it to the obvious baseline? I’d love to hear how you found out.