F1 Unleashed: How I built it with Claude Code
Introduction
F1 Unleashed: How I built it with Claude Code

Qualifying at the 2026 Monaco Grand Prix
Introduction
I’m a big Formula 1 fan. Fanatic may be a strong word, so I’ll use enthusiastic. And I’m also a data guy, having worked in analytics for nearly 20 years. Formula 1 is a data-heavy sport, with a lot of real-time information that has to be considered. It’s actually interesting that a lot of people don’t enjoy Formula 1 because “it’s just cars going around in circles”. Which is a fair assessment. In my opinion, it only gets interesting if I have the timing screens next to it to provide context. Who’s catching up, who’s losing ground, what might happen in the pit stops, who chose the best strategy, etc.
So I decided to combine the two with the help of Claude. And went on to build F1 Unleashed, a live-timing and replay application with synchronised audio commentary (and soon, also synchronised video) that goes deeper than the normal live-timing screens.
The short story? Building with Claude is much easier and faster than doing it manually, no question about it. But writing the code is the easy part. The difficult bits? Everything else.
Disclaimer
I am not, nor is this project, in any way affiliated with the FIA, Formula 1, any Formula 1 teams, or any other entities involved in the organisation and management of the competition. All the data used is publicly available, either free of charge or via an F1 TV subscription. I’m just a fan looking to improve my viewing experience and hopefully help others achieve the same goal.
During the development of this tool I did not circumvent any geographical restrictions, nor did I access, or try to access, any data I was not duly authorised to read. I have access to the full live dataset as part of my subscription to F1 TV.
First successes
Progress was rather quick in the beginning. I described what I wanted to Claude, corrected a few of its assumptions, added more detailed info as we progressed, and in a matter of days we had working track maps, a data-streaming engine, a caching mechanism and a client that could display data either live or on replay.
The caveat is that Claude doesn’t know the first thing about Formula 1. So every algorithm needs to be explained in detail, and oftentimes I had to interrupt Claude’s work because it went off in a completely different (and very wrong) direction.
I deferred some features to a later stage (see challenges below), once I built a reliable engine, but all in all, progress in the first week or two was so fast that I honestly thought I would be able to test it live during the pre-season testing (two 3-day testing sessions in February) and then have it finished by the time of the season opener in Australia in early March.
I was wrong.
First challenges
Accessing the data
I normally use a VPN. It’s always on, regardless of where I am (or where I want to be), for security and privacy.
But most F1 content is now in an arms race against VPNs. I’m guessing quite a few fans use VPNs to circumvent geographic restrictions, accessing F1 TV in countries where it doesn’t hold the rights, or maybe fishing for cheaper subscription prices in different markets.
So, VPN is on? If it is, I can’t access any data. I just assumed there was something wrong with the SignalR client and spent a whole day debugging code that had no problems whatsoever.
Track maps
Another early challenge was finding a good source of track maps. Something in SVG, with a known coordinate system I could use to both draw the track and overlay car markers, weather radar images, yellow flag sectors, etc.
There is no official source of data for that. But the MultiViewer community did amazing work in creating a high-quality dataset.
Audio streams and sync
There’s a commentary audio stream, which comes with its timestamp markers. But it’s normally delayed by over 1 minute behind the live broadcast. Getting the right timestamps and matching them required some trial and error.
The issue only surfaced during live sessions; replays were not affected. With so few live sessions and the first three events happening at awkward time zones (Australia, China, Japan), the chances to test and fix the issue were limited.
And then there’s the data.
The data

Raw Formula 1 timing data
I’ve always assumed that Formula 1 data would be quite easy to process. After all, it’s the pinnacle of motorsport, the very definition of high tech.
But reality is much different. Yes, there is an official data feed, parts of which are publicly available; others require an F1 Access or F1 TV subscription. But there is no official documentation anywhere; all available information is due to the efforts of a community of fans: Fast F1, Undercut F1, Open F1, MultiViewer, among others.
To start things on the right foot, there isn’t one data stream, but a collection of different topics, covering different datasets. Some of those carry reliable timestamps, others don’t. And the streams aren’t in sync with each other.
Here are a few real examples:
- TimingData topic is the official Formula 1 timekeeper. Its lap times, sector times and lap counts are authoritative. But they don’t carry a reliable timestamp.
- Telemetry and GPS position data do carry a reliable UTC timestamp. But they’re not in sync, with each other or with TimingData.
- A new lap may be recorded by TimingData slightly before or slightly after (in some odd cases a long time after) a car is seen crossing the start/finish line in its GPS data.
On top of that, all topics only emit deltas. All variable values are sticky: if they’re absent, the previous value still holds:
- So when Sainz drove two consecutive laps with the exact same lap time, the second lap didn’t report a lap time value.
- If a driver improves his best lap time in each of several consecutive laps, the PersonalBest flag is present in the first of those laps. In the others it’s just absent (which means it’s still true).
- In the predicted championship standings, the names of each team are only communicated in the first message. From then on the match must be done by the keys in the message’s payload.
A value is only overwritten if done explicitly. Which is why when a driver gets into the pits we receive a message saying InPit: true; when he comes out of the pit lane we receive InPit:false, PitOut:true; and shortly after we receive PitOut:false.
Lap times are emitted even for laps that are not timed (either start or end in the pits). But the point where the start/finish line is placed depends on the circuit geometry. Some garages are placed before the S/F line, others after.
What this means is that in practice and qualifying sessions the time spent in the garage will be added either to the lap time of the in-lap, or to the lap time of the out-lap.
And a personal favourite? Same message topic, same message type, same variable, same value, different meanings:
- When timing data communicates a lap number in practice or qualifying, it means the driver has just started his lap.
- But in the race, it means he just finished it.
So, a LapNumber of 3 means “2 laps done, lap 3 underway” in non-race sessions but “3 laps done, lap 4 underway” in races.
Finally, there are the data outages. TimingData works like a clock (pun very much intended), but other streams, such as Telemetry and GPS position, often suffer outages that can last from a few seconds or minutes up to a whole session and may affect only a car or two or extend to the whole grid.
In the process of better understanding the data, I also understood Claude’s limitations. If I wrote a prompt like “Lap 5 of car 6 in Melbourne FP1 is wrong”, Claude would reach the conclusion that “the driver was pushing, which is why the lap time is so low” when in fact it was a double counting of a start/finish crossing that showed as a normal 1-ish minute lap followed by a 20-second one. The actual root cause was… Discombobulating… Almost done thinking… a quirk of the GPS position data that is unreliable around pit entry and needs to be properly gated.
So, Claude, you’re wrong a lot. And when I say “you’re wrong”, you reply “You’re right, I was wrong in my conclusion. The real root cause is” and provide an even “wronger” answer.
Something had to be done.
Teaching Claude to say “I don’t know”
Claude assumes lots of things, and very often quite wrong. Especially in complex domains, a general-purpose model is often led into surreal tangents or hilariously wrong conclusions.
At one point, after being asked to model a lap time based on incomplete data, Claude actually said “Great news! The data fits the model perfectly! In more than 50% of the cases we’re under 2s of error and in 75% we’re under 3s”.
Yes, you read that right. Under 2s of error half the time predicting a lap time in F1 is what Claude called a successful model fitting. For those who are not fans of F1, 2s per lap is often larger than the gap between the fastest and slowest cars on track.
So, how do you teach Claude to stay in its lane and not make stuff up? How do you teach Claude to go against its own incentives and stop pandering to your preconceived ideas or going on a bug “fixing” spree that only makes matters worse? It would seem Claude understood the question, read the code and the data, and then… went off in the opposite direction!
Once, after telling Claude that there was something wrong with the way telemetry identified start/finish line crossings and asking for data to analyse, Claude analysed the data, reached a (wrong) conclusion, and went on to fix a bug that did not exist, leave the actual bug untouched, and create a dozen or so new bugs in the process.
Not that Claude got it wrong all the time. A lot of its analysis was good, pertinent and helpful, at least in part. But the times it did get it wrong, it set us back quite a bit.
Teaching an AI to ask, instead of assuming, can be tricky. You have to set the roles quite clearly: the human is the architect of the application and the subject matter expert. Claude is the developer. Claude should only implement what is asked, and if there are multiple ways to achieve a result, it should present the alternatives to the human instead of picking what it thinks is best. Write it down clearly in CLAUDE.md, so it gets picked up in every session. And add any relevant learnings to the agent's memory for reference.
Roles became clear, division of labour was respected, and Claude’s output became better in the process. It was time to tackle the big tasks.
Full refactor? No biggie
After the first few iterations and having built a reasonable client, I thought the hardest part was done.
So I started adding all the features I wanted to include. Lap-time predictions; tyre-wear analysis; qualifying and race simulations; pit strategies.
And that’s when I understood a few things, which can be quite hard to fix:
- As I added more features, the client grew heavier and heavier. I started with a thin backend and I did all the number crunching in the client. But this just made the client hungrier for computing power and memory, causing it to crash often.
- Due to the delta nature of the data, skipping forwards required processing all messages in that skipped interval. Going backwards was even worse: to skip backwards 1 minute the engine had to go back to the start and reprocess all events until the desired target.
- When I looked into the data in more detail, I understood the real challenge: an AI cannot correctly analyse a dataset from a complex domain of which it has no specific knowledge. I had to be more hands-on: not just defining the architecture and describing the features, but being more concrete in defining the algorithms used to estimate, for example, the exact moment a lap started or ended.
And so the refactoring process began. The first approach was to save client snapshots at regular intervals. It improved performance but at the expense of 2 to 3 GB of extra memory needed by the client.
So this wasn’t working. Time to reverse the approach: server pre-processes the data and ships ready-to-consume messages to the client.
Refactoring the entire codebase took maybe 2 or 3 days. But that’s because I was not working on this full time: I wanted to test it every step of the way, replaying several sessions, and I forced Claude to pretty much ask permission to rename a function.
It worked. More or less. Now that the heavy loads are on the server, the complexity and fragility of the algorithms became apparent. And the bugs.
So I had to refactor the backend. Again. One by one, I replaced each data pre-processor with a more robust, more reliable, simplified version and kept on going. And it worked.
That’s where an AI companion really excels. It may fail at the complex questions. It may need lots of supervision when trying to solve those.
But when the task at hand is above all large, mechanical, repetitive and needing a human’s undivided attention for long periods just so obvious mistakes don’t happen? A coding agent is the right tool for that job.
In a hurry I released version 1.0 on 7 June, the day of the Monaco GP. It was the 60th anniversary of McLaren’s first race, and simultaneously their 1000th Grand Prix start. And, as a McLaren fan, I had to mark the occasion.
But it wasn’t ready, to be honest. A lot of rough edges, still some data reliability issues to address, no audio sync, and the backend still needed that second refactor I alluded to above.
In the week after the Monaco Grand Prix, Claude and I finished the engine refactor, cleared most of the open issues, got the audio in sync with the data, and simplified and improved the architecture and codebase.
That sync issue that had been plaguing us? Turned out it was much simpler than I thought. And definitely simpler than all of Claude’s theories and hypotheses that sent me into a very deep rabbit hole of complexity. It was only due to the audio stream delay: it’s downloaded in chunks and arrives about 1 minute behind the live action. Whereas the data usually arrives between 20 and 30 seconds behind live. So, the stream was trying to sync to a point that it hadn’t downloaded yet! A one-line change fixed it: sync to the earliest of the leading edges of both streams.
As for the final architecture, here it is in a nutshell:

The data flow through the application
- The SignalR client fetches live data streams and stores the incoming messages locally.
- The cached data file is read and each message is emitted via an event bus.
- Different data processors listen to one or more of the incoming message topics, process them and emit a new message that holds the results, ready to be consumed by the client.
- Some processors listen to the outputs of others, but without circular dependencies anywhere.
- The client receives processed messages and just renders them, which results in a relatively lean web page.
The best part? Because downloading/streaming was decoupled from parsing the data file, for the client there’s no difference between playing a session live, playing it with a delay or replaying it later.
The processed data is stored in a transient database that is cleaned up when the client disconnects. And when a client connects, the database is rebuilt and starts streaming data to the client immediately, resulting in almost instant playback.
Lessons learned
What made it all work was being able to understand what I still had to do myself, what Claude was supposed to do, and limit its scope of action. Above all else, do not make assumptions; ask for clarification.
It also helps to have a set of basic rules of engagement written in CLAUDE.md and to explicitly tell the engine to add some learnings into its memory for later recall.
To me, the most important lessons to retain when starting to code with the help of an AI agent are:
- AI does not replace human analysis, nor knowledge. If anything, it makes that analysis more critical. If the prompt is vague, the AI will interpret it and go with that interpretation, with unforeseen consequences.
- Don’t trust the AI blindly. Be critical, sceptical, even cynical. Assume it’s wrong until proven right. Ask for clarification. If Claude suggests a course of action and you don’t understand what it means, ask for more detail. But don’t let it implement things you don’t fully understand.
- Don’t be in a hurry. The scaffolding and early prototype can be rolled out really fast, but that may give a false sense of progress. The hard part is yet to come.
- Commit early, commit often. And use feature branches as soon as you have a stable scaffold. New features should be, as much as possible, independent. This also forces you to prioritise: core features need to be implemented first. Nice-to-haves and visuals can be left towards the end.
- Use an issue tracker. I use Trello and though somewhat feature-limited when compared to the heavyweights, it gets the job done easily. Atlassian’s
atlassian-trello-mcpallows Claude to read, edit and comment on cards, which helps keep the sprints organised. - Take risks! Because refactoring code becomes cheaper, don’t be afraid to try out crazy ideas just to see them fail after a day or two of rebuilding the whole application. If you get it wrong, roll back and continue the old path. If you get it right, you gain a more mature application and reduce future technical debt.
- Test. A lot. Test everything. If you describe a feature and an AI implements it, make sure to spend enough time thinking about all possible edge cases. And look out for regressions. Look out for any modified files that shouldn’t have been touched.
- Take it one step at a time. Learn how to build one app well before you decide to embark on a sprawling army of autonomous agents.
Roadmap
It pays off to understand data engineering and architecture well. Now that the application works and the data is streamed faithfully, the real fun can begin: analyse lap and sector times, corner apex speeds, tyre degradation patterns, and build models to gain insights into strategy, tactics and the flow of the race.
That was, after all, my original goal. To create an app that does in real time what I normally do in my head, hopefully with better accuracy. Have it fire an alert when there’s an undercut/overcut opportunity; when a driver’s pace shows the tyres going off the cliff; and, my true holy grail of F1 data analysis: to be able to predict the exact point a wet/dry crossover happens.
Then it won’t be just me screaming at the TV that McLaren got their Silverstone pit stops wrong, but having the hard data to back it up (replace McLaren with Ferrari and Silverstone with any race, and the meaning remains the same).
The most important items in the agenda are:
- Quali and race pace analysis: from the free-practice data, predict each team’s real performance.
- Tyre performance analysis: estimate the real pace advantage between different compounds and use that knowledge to analyse different strategies.
- Race analysis in real time: identify fuel- or tyre-saving driving styles, pit-stop opportunities and predict where the action will most likely happen on track.
- Wet weather predictions: this will be the crown jewel. To be able to identify the dry/wet sweet spots where a tyre change is the best course of action.
And perhaps the most exciting functional feature is not yet developed: automated video sync. Choose the window broadcasting the TV coverage of the Grand Prix and it will pick up timing markers, sync the client to the video automatically and check sync accuracy periodically.
Try it out!
The code is open source and it’s available on GitHub.
Clone the repo, install the required dependencies and enjoy your new F1 viewing experience!
Thoughts, suggestions, criticisms, bugs? You can file an issue on GitHub. Any feedback (and testing results in other setups) is welcome and will help improve the application.
Do you have a crazy idea about an amazing app but are a bit wary of jumping into the AI-assisted building bandwagon? Drop me a line on LinkedIn.

2026 Monaco Grand Prix Race
메타데이터
- post_id
- fa1ca26fdc1b
- slug
- f1-unleashed-how-i-built-it-with-claude-code-fa1ca26fdc1b
- url
- https://medium.com/@nsousa/f1-unleashed-how-i-built-it-with-claude-code-fa1ca26fdc1b
- canonical_url
- https://medium.com/@nsousa/f1-unleashed-how-i-built-it-with-claude-code-fa1ca26fdc1b
- author_url
- https://medium.com/@nsousa
- status
- ok
- fetched_at
- 2026-06-20 20:29:01