Josh Magidson runs the league, and Josh asked me to build it. That is its own data point. When the person running a thing asks a specific person to make the thing, they are not commissioning software, they are extending trust. Josh knew I could, and he knew I would, and the gap between those two things is where most projects live or die. This one had four days.

Other Half Draft is a small thing with a clever rule. It is a ten-manager private fantasy league, and on the final day of the Premier League season, all ten fixtures kicking off simultaneously at 16:00, every manager is randomly assigned one. The title was already settled. Everything else was not. Seven of the ten fixtures that afternoon carried live stakes: Champions League places, Europa spots, Conference qualification, a relegation battle still open. The football would have mattered even without the draft layered on top of it. The first goal in your fixture puts you on the board. Every subsequent goal moves you up one position, swapping with whoever is directly above. A red card moves you down one. Whoever sits at position one when the whistle blows on the last game of the season gets first pick in next year's draft. It is a closed system with simple inputs and an enormous amount of drama packed into ninety minutes.

I built a live tracker for it. The brief I gave myself was modest: show the ten fixtures, show the live board, fire a foghorn and some confetti when a goal lands, and let everyone in the WhatsApp group watch the positions move in real time. Each manager's card had a real photo composited on a shirt silhouette, with a team-colour border matching the fixture they had been assigned. You could glance at the board and know immediately whose game was whose. I deployed it at 01:46 BST on 20 May 2026, four days before kickoff. By 16:00 on the 24th it was working perfectly. By 17:22 the group had stopped looking at it. This is a story about why.

The stack was deliberately boring. One HTML file, around 50KB, all CSS and JavaScript inline, no framework, no build step. The reasoning was not aesthetic, it was operational. Something that needs to be correct in front of an audience of ten people watching ten simultaneous football matches should have as few moving parts as possible. Every dependency is a thing that can fail at 16:07 on a Sunday afternoon with no opportunity to push a fix. A single file you can read top to bottom in a sitting is a file you trust. Three Netlify serverless functions sat behind it. Two Netlify Blobs stores held the state. That was the whole thing.

The data architecture was the part I worried about most. The primary source was API-Football Pro at nineteen dollars a month, an upgrade from football-data.org's free tier. The Pro tier refreshes faster and returns verified goal event data: the minute, the player, the type. If you are firing a foghorn on a goal, you want the event to be real and you want it on time. But paid APIs go down too, and the league does not pause for rate limits, so the function falls back to football-data.org automatically on any error from the primary. The client surfaces which path served the response, so a glance at the page tells you whether you are on the good source or the backup. The board never goes dark. It degrades.

The state layer almost broke me before the match even started. Netlify Blobs defaults to eventual consistency, which is fine for most things and catastrophic for this. A POST to set the draw would succeed, and a GET from the next function invocation a second later would return the previous state. The board would flicker between truths. The fix was a one-line switch to strong consistency mode, verified with a curl GET-after-POST that returned the new state immediately. Tiny change, total behaviour difference. The kind of bug that does not exist until you actually try to write something and read it back.

The audio was three distinct moments and three different generation strategies, all using my ElevenLabs voice clone. The kickoff intro was pre-generated, baked into the deploy, ready to fire the moment the first whistle blew. The half-time bulletin was generated live during the match from the actual standings at the actual minute it ran, which meant it could say something true about what had just happened rather than reading a script written days earlier. The winner reveal was generated live at full time with the winner's name baked in. The half-time piece was not in the original spec. I added it because it felt like the kind of thing that would make people laugh, and laughing at a thing makes them stay.

The interesting bit of code is the ranking engine. The naive instinct is to treat position as a function of inputs: take each manager's entry order, subtract extra goals scored, add red cards, sort. That instinct is wrong, and it is wrong in a way that took me a while to admit. The position of any manager at any moment depends on the entire ordered history of goals across all ten fixtures. If Lee's fixture scores at minute 30, his swap with whoever is above him at minute 30 is not the same as his swap with whoever is above him at minute 70. The board is a sequence of displacements, not a sum.

The only correct implementation is event replay. Sort every goal and every red card chronologically by timestamp, walk through them in order, apply the swap, move on to the next event. The state at any moment is the result of replaying every event that has happened up to that moment. This is more code than the formula version, and there is no shortcut. If you try to summarise the history into a number, you lose the information that the history actually contained. That is what I had built. It worked. The Playwright screenshot proved it worked.

The morning of the match, Claude Code ran an end-to-end test against the live deploy. All ten API endpoints hit, returning TIMED for every fixture. Draw lock confirmed: a POST to set-draw returned 409, meaning nobody could change the assignments mid-match, accidentally or otherwise. Fake goals injected through the admin endpoint. The board verified via Playwright screenshot showing JP at slot one, Lee at slot two, me at slot three, exactly as the event replay predicted. No errors. No fallback. Green across the board. The draw itself had been set and locked two days earlier, on 22 May. Everything that could be tested had been tested.

I knew this on Tuesday. I had to relearn it on Saturday.

The first half was the most fun I have had with software in a long time. Aaron scored first, around 16:10, Burnley against Wolves, and the whole thing came alive at once. He jumped to slot one. The foghorn fired. Confetti rained down the screen. The goal banner did its thing. And the board, the actual live board, shifted in real time with Aaron's name climbing into first.

Within two minutes of that goal I had rewritten the kickoff voice line with Aaron's name baked into it, generated the new audio through ElevenLabs, and pushed it live. The group heard my cloned voice telling them Aaron had drawn first blood while the match was still being played. The broadcast had not even cut away from the celebration. That was the moment I knew the thing was actually working.

Then the goals started rolling. Around 16:20 Fulham scored against Newcastle and I went to slot two. Four minutes later Man City scored against Villa and Lee slotted in at three. Then Sunderland against Chelsea at 16:26 and Dan G took four. Blake came in at five when Forest scored against Bournemouth around 16:34. Baum took six a minute later off the back of Man Utd against Brighton. Steven and Phil came in almost together just before half-time, Spurs against Everton and Arsenal against Palace landing within the same minute, filling seven and eight.

And then the bit I had been waiting for actually happened. Baum scored a second goal around 16:45 and jumped from slot six to slot five, swapping places with Blake live on the board. The swap mechanic, the thing I had spent hours getting right, fired exactly as designed. No refresh. No glitch. Just two names trading positions in front of everyone.

Somewhere in the middle of all that I noticed every single fixture had transitioned to PAUSED at the same minute, all ten of them, and I realised I could build a half-time audio bulletin that had not been in the spec at all. So I did. While the match was running. I wrote the detection logic, added a button that only appeared at half-time, wrote a bespoke script template that pulled in the real standings, generated the audio through ElevenLabs, and deployed it before the second halves kicked off. Zero downtime.

The half-time board: Aaron first, me second, Lee third, Dan G fourth, Blake fifth, Baum sixth, Phil seventh, Steven eighth. JP and Commish still in the pool, their fixtures goalless. The group was watching. It was doing the thing.

And then I broke it.

Here is the exact sequence, because the lesson is in the sequence. A few minutes into the second half, Baum's fixture scored a second goal, and Baum moved up one place. I looked at the board and decided it looked wrong. I cannot defend this except to say that in the moment, with the group watching, the cost of the board being wrong felt higher than the cost of touching it. I convinced myself that positions should lock after the first goal in each fixture. I changed the engine to first-goal-only and deployed.

Josh corrected me on WhatsApp almost immediately. Every goal moves you up one, red card moves you down one. I knew this. I had built it. So I re-added the move-up mechanic. But here is the second mistake, which is the real one. Instead of reverting to the event-replay code that had been correct since the 20th, I rewrote the engine as a formula: entryRank minus extraGoals plus redCards. It looked cleaner. It was wrong for the reason I described earlier: you cannot simulate ordered displacement with arithmetic. The board started showing me at second when I had no business being at second.

At 17:22 Josh sent a voice note. You score first, you go to position one. My game scores second, I go to position two. Lee's game scores and he gets into position two, you go into position three. Every goal up one, red card down one. He was describing, precisely, the algorithm I had deleted forty minutes earlier. I deployed the correct event-replay fix at around 17:10 on my third push of the half. The board was right. It was now showing the truth.

The code being correct and the group trusting the code are two different states, and you cannot bridge them on the timescale of a football match.

It did not matter. Josh was already maintaining the league standings by hand in WhatsApp. The group had stopped refreshing the tab. Lee's fixture, Manchester City against Aston Villa, swung late; City equalised to make it 2-2 in the closing minutes and Lee went to first at around 16:50. He stayed there. Final order: Lee, Aaron, Baum, me, Dan G, Phil, Blake, Commish, JP, Steven. By that point my tracker was correct and irrelevant in equal measure.

The honest part is this. The original code was correct on the first deploy. It had been tested. The Playwright screenshot confirmed it. The right response to seeing Baum move up on a second goal was to think for half a second and recognise that this was the rule working as designed. Instead I treated a working feature as a bug, changed it, broke it, patched it wrong, deployed three times in forty minutes during the live event, and by the time the correct fix was on production the audience had left the building.

There is a version of this story where the lesson is about event replay versus formulas, and that lesson is real. Picking the right model up front matters, because the wrong model will feel cleaner and will lie to you in plausible ways. Event replay is the right shape for any system where order matters and entities affect each other in sequence. The instant I reached for a formula I was telling myself a comforting story about the data, that it was simpler than it actually was. A leaderboard where ten people displace each other on every event is not arithmetic. It is a state machine. I knew that on Tuesday. I will write it on the inside of the laptop lid for next May.

There is a second honest thing, and it is harder to sit with. Even if I had never touched the code at half-time, the board would still have been wrong at some point. API-Football Pro logs a goal the moment it happens on the pitch. If VAR reviews and disallows it, the API does not reliably update or remove the event. The tracker had no way to know a goal had been ruled out. At least one disallowed goal -- Crystal Palace, VAR offside -- appeared in the feed and moved a manager on the board. The positions shifted on the strength of a goal that never counted. The group saw the board contradict what they were watching on screen and had no explanation for why. That alone erodes trust, independent of any logic bug. The data source is not a referee. It reports what happened on the pitch before the referee's final decision. For next season, someone needs to confirm each goal before it hits the board. The API tells you a goal happened. A human tells you it counted.

But the bigger lesson sits one level up. Confidence in a piece of software leaks out of a group faster than it can be poured back in. The first wrong board state cost me some credibility. The second cost me most of it. By the third deploy I was the only person still looking at my own site. Trust is asymmetric. It accrues slowly through small correct behaviours and it evacuates in one bad ten-minute window. There is no API for getting it back. No deploy, however correct, can recall it within the same match.

The cost of a change at 16:47 on match day is not the same as the cost of the same change at midnight the night before. At midnight the cost is your own time. At 16:47 the cost is the trust of ten people who are simultaneously watching ten football matches and one website, and have a low tolerance for the website being the part that fails them. I knew this before I started. I knew it while I was typing the formula. I deployed anyway, because the in-the-moment cost of doing nothing felt higher than the in-the-moment cost of doing something, and that calculation is almost always wrong during a live event.

I will build this again for next season. The mechanic is too good not to. The half-time audio bulletin will ship as a planned feature. The foghorn will stay. The ranking engine will stay exactly as it was at 01:46 BST on 20 May, because it was right then and it is still right now. And when the games kick off at 16:00, I will close the editor, put my phone down, and watch the football. The job on match day is not to improve the system. The job on match day is to let the system do what you already proved it could do.