Motorsport data · Open
I shipped a live Formula 1 statistics site without writing a line of code, to find out whether product thinking is the scarce input
An experiment in whether product thinking alone can take a product from zero to one, with AI in every engineering role.
TL;DR
- I am a UX designer who does not write code, and I wanted to know whether that still had to be a limit.
- I supplied only direction, research and judgment, and let AI occupy every engineering role. I never touched the code.
- Every season since 1950 is live, validated against independent sources, and updates itself the morning after each race.
- Every failure in the project came from my specification, not from the execution.
Context
The hypothesis, and why Formula 1
I’m a UX designer. I don’t write code. I wanted to find out whether that still had to be a limit.
The question wasn’t “can AI write code for me” — everyone knows it can. The question was sharper: if I supply nothing but product thinking, and let AI occupy every other role on the team, what kind of product comes out the other end?
So I set the rules before I started. No developer. No QA engineer. No PM. No second designer. Claude would play all four. I would contribute direction, decisions, research, and judgment, and nothing else.
I picked Formula 1 historical data as the domain for two reasons. The honest one first: I’m a serious F1 fan, and an experiment you’ll actually stick with for months has to be about something you care about on a random Tuesday night when nothing is working. The second reason is that the domain happens to be ideal for the test — the data is real, it’s messy, and it has a brutal quality bar. F1 fans notice a wrong number instantly. If the experiment produced something subtly inaccurate, the audience would tell me.
The goal was a site where fans could browse every season and race since 1950.

Constraints
One rule I set myself and never broke
I stay out of execution entirely
This was harder than it sounds, and for a specific reason. I’m not a developer, but I’m not completely blind to code either — I’ve written enough in the past to read what’s happening and to have opinions about it. So every time something broke, the instinct was to open the file and poke at it myself.
I never did. Not once. The whole point of the experiment was to find out what AI produces when a human doesn’t quietly patch the gaps, and every small intervention would have contaminated the result. If I’d fixed three things by hand, I’d have finished the project unable to say whether it worked.
What I actually did, across the whole project:
- Defined what to build and, more importantly, what not to build yet
- Researched the domain and the data landscape
- Made every call where two reasonable options existed
- Ran every command and read every output
- Verified results against the real world
What Claude did: all SQL, all Python, all Astro, all deployment config, all debugging.
The finding that surprised me is that this split is stable because the two halves fail differently. Nothing in this project broke because of syntax. Everything that broke, broke because of a decision — what signal to key a job off, what to do when a source contradicts itself. Execution is delegable. Judgment isn’t, and there’s more judgment in building a product than most people assume.
Options
Where the data actually comes from
This was the longest research phase and the one that determined everything downstream. There was no obvious answer, so I worked through the options properly.
Option A: Jolpica’s free API, live. Fast to start, no database to run, but I’d be permanently dependent on someone else’s uptime and rate limits, and I’d own nothing. It would also mean I’d never test the harder half of my hypothesis, which was whether AI could help me stand up and operate real infrastructure.
Option B: My own database. More setup, more responsibility, full control. And the only version where the experiment was actually interesting.
I went with B. Then the sourcing problem split in two.
History: the Ergast dataset covers 1950 to 2024 and is well structured. I imported it into Supabase and that gave me seventy-four years in one move.

The gap: 2025 to today. Three routes here, and I weighed all of them.
- Collect and enter it manually. Guaranteed accurate, completely unscalable, and pointless work for a human to do.
- Scrape it. Fragile, and I tested this — complex tables with embedded images and flags get silently mangled, which is the worst failure mode because it looks like it worked.
- Backfill from the Jolpica API with a Python script.
I chose 3, and the reason matters: filling the gap and staying current going forward are the same problem. A script that could fetch 2025 and 2026 was already most of a script that could fetch next Sunday’s race.
Staying current: this led into researching scheduled jobs, and then into a question I hadn’t anticipated. Getting new data into the database is only half the job. The site is statically built, so fresh data sitting in Supabase changes nothing on the live site until a rebuild happens. That’s how I ended up learning about deploy hooks, and the loop closed: new race detected, data loaded, deploy hook fires, site rebuilds with the new results. Claude built all of it. I designed the loop.
The most valuable thing I did here wasn’t technical. It was refusing to accept the first workable answer and instead mapping the options against what I actually wanted the product to be.
What I chose
One vertical slice, and a written rule to refuse the rest
The tempting build order was horizontal: load all the data, then build every page type at once. I went vertical.
Shipped: year index, season page, race page. Complete, working, usable today.
Deliberately deferred: driver statistics, circuit and venue history, head-to-head comparisons, qualifying and pit-stop detail, search.
I wrote “do not build ahead of the current phase” into the project rules and treated it as binding. The data model was designed so all of those remain possible — deferred is not the same as unplanned — but none of them got built.
On a solo project there’s nobody to say no to you. The written rule was the mechanism that said no.
What I found
The wrong answer that renders perfectly
Understand the shape of the data before trusting a query
One example stands in for a whole category of risk.
Championship standings in this dataset are stored cumulatively, attached to each individual race rather than to the season. Query it the intuitive way and you get whoever was leading at whatever race you happened to hit — presented, confidently, as the champion.
It’s the worst kind of bug. The page renders fine. The numbers look like numbers. It’s just wrong, and only someone who knows the data model would catch it.
The lesson I carried forward: with AI doing the building, the plausible-looking wrong answer is the real risk, not the crash. A crash announces itself. A wrong champion doesn’t. That shifted how I spent my time — less watching the build, more checking the output against reality.
Proving the data before trusting the product
Before I’d believe any of it, I ran a manual validation pass. I picked 20 races and drivers at random across different eras, opened my site and an independent source side by side, and compared everything: finishing positions, points, championship totals.
Every one matched.
That single afternoon changed the project. Up to that point I was hoping the data was right. After it, I knew, and I could build on top of it instead of second-guessing it. It’s the step that’s easiest to skip and the one that made everything after it possible.
The design
Three decisions made for business reasons, not technical ones
Static, built ahead of time. All data is queried at build time and shipped as plain HTML. No database call when a visitor arrives. Pages load almost instantly, which for a content site competing in search is a growth decision.
URLs locked before any page was built. /seasons, /2021, /2021/bahrain-grand-prix. Readable, guessable, permanent. URLs are the one thing that’s genuinely painful to change after launch, so they got decided first instead of last.
Hosting costs nothing. Deployed on Cloudflare Pages, with the database on a free tier. The entire stack — build, deploy, hosting, database — sits inside free tiers, so there is no recurring bill at all. I’d assumed shipping a real, live, automatically-updating product meant monthly hosting costs, and it turns out that for a static site of this size it doesn’t. For anyone testing an idea, that removes one of the last real excuses not to ship.
What I'd do differently
The edge case I missed, and what it cost
A few months after launch, the result of the 2026 Monaco Grand Prix changed. A penalty was reinstated on appeal, which changed the classification, which changed the points, which changed both championship tables from that race onward.
My pipeline had no answer for this, and the reason is instructive. I had designed it to detect new races. I had never considered that an old race could change.
The job ran, saw no new race, and did nothing. My database sat there confidently serving a result that the sport no longer recognised.
Fixing the one race was trivial. The real problem was that a hand-patched value is fragile — the next automated run would treat my correction as drift and overwrite it with the stale upstream number. So the fix had to work at three levels, and I directed each one:
A record of every deliberate disagreement. Corrections are logged in a dedicated table that marks exactly where my data intentionally differs from upstream. The pipeline reads it and treats those values as protected rather than as errors to reconcile away. The reframe that made this work: the pipeline isn’t syncing with upstream, it’s syncing with upstream plus a documented set of exceptions. Once the exceptions have a home, they stop being technical debt and become data.
Active verification instead of passive trust. The job no longer only asks “is there a new race.” It now checks existing results and championship totals against the official source, and when something doesn’t match, it goes back, finds what changed, and reflects the correction.
A full-season sweep before the next season starts. Appeals and rulings can land months late. So at the end of each season the script re-verifies every race of that year in one pass, confirming the historical record is settled before a new year of data starts stacking on top of it.
The honest version of this story is that this was a design failure, not a bug. Nobody wrote the code wrong. I specified a system that assumed the past was immutable, and the past isn’t. Thirty minutes of asking “what if something already loaded turns out to be wrong” during the design phase would have caught it. It cost me a live inaccuracy and a rebuild instead.
The part I didn’t plan: contributing back
Working through the Monaco correction, I realised the free API I’d been relying on carried the same stale result I had. The difference was that I’d found out and they hadn’t.
I’d spent the whole project consuming open data for free. So I submitted the correction upstream — my first public open source contribution.
The shift in perspective is worth naming. For most of this build I thought of data sources as infrastructure that either works or doesn’t. Finding an error and fixing it for everyone reframed them as something closer to a commons, maintained by whoever bothers. Being the person who found the problem made me responsible for it.
What changed
Did the experiment work
- Every season since 1950 is live at enaracing.com
- Built and published in two weeks, after two months of thinking about it
- 20 of 20 random validation checks matched independent sources
- New race results are live by the next morning, with no manual step
- One data error found and corrected upstream, for everyone
- Total running cost: $45 for the domain. Everything else is free.
Yes, with a condition. Product thinking plus AI got a real product from zero to one. But the experiment doesn’t prove that direction is enough on its own — it proves that direction is the scarce input. Every failure in this project came from my specification, not from the execution.
My conclusions
The bottleneck moved. Two months of this project was thinking. Two weeks was building. When execution is that cheap, the quality of a product is set almost entirely by the quality of the thinking in front of it — which is a very different constraint than the one most teams are organised around.
Design for the past changing, not just the future arriving. Almost every system I’ve seen specified assumes new data flows forward. Monaco taught me to ask, up front, what happens when something already recorded turns out to be wrong.
Validate once, properly, and early. One structured afternoon of comparison converted the whole project from hopeful to trustworthy.
Determination is the real input. There was no team, no deadline, and no one waiting on it. The hard part wasn’t any single decision — it was continuing to make decisions with nobody pushing.