For six weeks in June and July, I started every morning on the same page: a World Cup bracket I built for myself. Spain won the whole thing, 1–0 over Argentina in extra time at MetLife Stadium, and the tournament ended. Most side projects die at exactly this moment, not from a decision but from neglect. The feeds rot, the page breaks, and the repo becomes one more gravestone in a GitHub full of them.
I wanted the other ending. The dashboard is now a permanent archive at nawewe.xyz, and it will render the 2026 World Cup correctly in ten years. This post is about how it was built, what the data turned out to say, and what it takes to finish a project on purpose.

What it was
One page, bracket-first. All 104 matches with their consequences spelled out (advance, win the Cup, take third place), a live ledger during play, the Group A–L tables preserved as an archive once the knockouts began, and a Crowd Forecast tab showing what prediction markets thought, kept visually and technically apart from the official results.
The architecture was two copies of the same small pipeline. ESPN's public scoreboard feed on one side, Polymarket's public markets on the other, each running fetch, validate, transform, snapshot, then render. The rule holding it together: the two streams never mix, not in the data model and not on the page.
Both feeds were treated as hostile. Payloads get validated at the boundary before anything reads a field. Malformed records are dropped one at a time so a single broken match cannot blank the bracket. And a failed refresh never overwrites known-good data, because the worst possible state for a dashboard is confidently empty.
Built by an agent, held to a standard
I built this with an agentic coding workflow, and that was half the point. The repo carries an AGENTS.md contract stating the conventions, the data rules, and the verification gate. The tests are the harness: they define what the data shapes mean, so the agent can refactor boldly and fail loudly instead of silently. Seventy-nine tests, type checking, lint, and a production build run before any deploy is allowed to happen.
The honest ledger includes the failures. During one edit, a refactor corrupted a function body with garbage output. The type checker caught it within minutes. That is the whole argument for the harness, in one incident: assume the author, human or model, will eventually write nonsense, and make nonsense expensive.
Two principles governed the work, and they are now written into the repo. Pick the simplest design that solves the actual problem. Reason from what is true, the data shapes, the failure modes, the user need, rather than from habit. Complexity has to argue for its existence in a commit message. Simplicity does not.
What the crowd believed, and what happened
The archive's centerpiece is a Forecast vs. Reality tab, and it nearly did not exist.
When the tournament ended, I froze the Polymarket data as it stood: Spain at 100%, every other team at 0%, match markets already delisted. Perfectly valid data, perfectly useless. A snapshot of a prediction market taken after the event is not a prediction. The crowd's beliefs had already been erased by settlement.
The rescue was price history. While the API was still alive, I pulled the daily price curve of the tournament-winner market for all 48 teams, June 1 through July 20, and froze that instead. That curve is what the crowd actually believed over time, and it told a story nobody scripted:
- The crowd backed France for the entire knockout phase. France was the favorite on the eve of every round, peaking at 39% before the semifinals. France lost that semifinal 0–2 to Spain, and 39 points of crowd confidence evaporated overnight, the biggest single-day move of the tournament.
- Spain was never the favorite until it almost didn't matter. The champions never ranked above second in the crowd read before any round. Only on the eve of the final did the crowd swing to them, at 59% against Argentina's 41%.
- Argentina at 9% was the run nobody priced. On June 1, the eventual finalists got less than a one-in-eleven chance.
One subtlety is documented on the page itself: daily prices are midnight-UTC snapshots, so a result lands in the crowd read the day after it is played. Every big move on the chart is annotated with the match that caused it, joined on that rule. Without the join, the ten biggest moves all appeared to have no cause at all, which is its own lesson about reading data you did not collect.

How you freeze a live thing
Archiving a dashboard is a small engineering problem with honest rules.
First, capture. Scripts run the exact production pipeline and write the final snapshots into the repo as validated JSON. The failure rules are explicit: empty ESPN data is fatal and aborts the freeze, an unreachable Polymarket is frozen as honestly unavailable, and no number is ever fabricated to fill a gap.
Second, serve the freeze. A build-time flag bakes archive mode into the deployed artifact. Both data endpoints return the frozen snapshots before any network call, so the live worker makes zero upstream requests. The footer states the freeze date, because an archive that pretends to be live is just a lie with good uptime.
Third, prove it. Boundary tests assert the frozen files are what they claim to be: 104 complete matches, every round populated, probabilities in range, the displayed freeze date matching the data.
Fourth, record the experience. A Playwright script tours the archive in a headless browser and produces the demo video and screenshots, so the record of what the live UI felt like cannot rot either.

The ending
There is a decision I did not make, and it is deliberate. The dashboard keeps the apex domain through the next cycle. In 2030, when the next World Cup arrives, I will choose: rebuild for the new tournament, or move this one to 2026.nawewe.xyz and let the archive step aside gracefully. Deferring that decision costs nothing. The archive does not care where it lives.
Finishing things is a skill, and it is rarer than starting them. The trophy for this project is not the bracket, the tests, or the pipeline. It is that the last commit was a decision.
The code is at github.com/AUAggy/world-cup-hub, and the archive itself is at nawewe.xyz. Unofficial fan project, not affiliated with FIFA, ESPN, or Polymarket. The forecast content is educational context, not betting advice.
← Back to journal