← All experiments

A World Cup prediction pool for hospital colleagues

I wanted a bracket pool for hospital colleagues for the 2026 World Cup knockout stage. I built a site that scored every bracket from live results, ran it to the final and then took it down.

  • Games
  • Data
  • Automation

Updated27 Jul 2026

The problem

I wanted a prediction pool for hospital colleagues, covering the knockout stage of the 2026 World Cup from the Round of 32 to the final. It had to take entries quickly, score everyone on its own as results came in, and stay fair as games locked one by one.

What I built

I built a website where anyone could enter with a username and a self-chosen passcode (no email), fill out all 32 knockout games and watch the bracket score itself. Points for each correctly predicted team doubled by round: 1 in the Round of 32, 2 in the Round of 16, 4 in the quarter-finals, 8 in the semi-finals and 16 for the champion, plus 8 for third place. Scoring counted each team you sent through a round, so one wrong early pick did not wipe out a whole branch.

Each pick locked 15 minutes before its own kickoff, enforced on the server so a changed phone clock could not get around it. When a game finished, the real winner moved forward and any pick that had become impossible was cleared, so players could pick again. The leaderboards broke ties by total correct picks, then by correct picks in the earliest rounds, and genuine ties shared a rank. A public stats page showed the most-picked champion and the most divided games, without anyone's individual picks.

Horizontal bar chart of champion picks in 125 of 134 brackets: France 57, Argentina 27, Spain 22 (highlighted as the team that won the cup), Brazil 4, Colombia 2, United States 2, and 11 other teams with 1 pick each.
The pool's favourite was France. Spain, picked by only 22 brackets, won the cup.
Column chart of the 134 brackets by final score in bands of 5 points, peaking at 26 brackets in the 35 to 39 band, with a dashed line at the median of 39 and a single highlighted bracket at the top band, the winner on 76 points. Six brackets with no picks sit at 0.
Most brackets finished between 30 and 50 points, and the winner was well clear on 76.

Equipment

How I built it

  1. I built the site in Next.js with a Supabase Postgres database, deployed on Vercel. The scoring and bracket logic are pure functions with unit tests.
  2. Codex (GPT-5.5) and Agy (Gemini) reviewed the code independently for security, results sync and data integrity, and I fixed what they found. The database refuses every request that does not come from the site's own server.
  3. I loaded the real fixtures and mapped them onto the official bracket tree, so the right winners met in each round.
  4. I pulled results from football-data.org on three paths: a GitHub Actions job every 20 minutes during game hours, a nightly Vercel job as the backstop, and a "Sync now" button for the admin.
  5. I gave it a clean look of its own, made the homepage mascots with Magnific (Nano Banana Pro) and designed the share card in Paper.
  6. After the final, I exported all the data with passcodes removed, wrote a final standings page and deleted the site and the database.

Results

The pool took 134 entries, and 99 of them (73.9%) were complete brackets. Spain won the tournament, beating Argentina in the final, and England took third place over France. France was the most-picked champion, in 57 brackets; only 22 picked Spain. The winning bracket scored 76 points, against a median of 39 and an average of 39.28. Every one of the 32 advancer slots was decided and scored.

The site and its database were deleted after the tournament, so there is no link to share.

What I learned

Free sports feeds have gaps, and they show up at the worst moments. After some penalty shootouts, football-data.org marked a game finished with no winner and a tied or empty shootout score, so I taught the sync to fall back on the full-time score; only a truly level result waits for the admin. Team names differ too: of my 32 teams, only "Cape Verde" appeared under another name in the feed.

Vercel's free plan runs scheduled jobs at most once a day. Live updates came from a free GitHub Actions workflow instead, with Vercel's nightly run as a safety net.

An export with credentials stripped and a final standings page let me delete the site and database without losing the result. The database setup scripts stay in the repository, so the pool can be rebuilt for a future tournament.

Lab notes

  1. I archived the final data and standings, then deleted the site and its database.

  2. On the day of the first knockout game I added a public stats page, tie-breaks where genuine ties share a rank, and cartoon mascots. Over the next two days I fixed the results sync for penalty shootouts that the feed marked finished without a winner, added a rules page and put each player's accuracy on the leaderboard.

  3. I built the pool (bracket editor, scoring, username and passcode sign-in, three leaderboards and an admin page) and deployed it on Vercel. After independent reviews by Codex and Agy I hardened it, loaded the real Round of 32 fixtures and set results to sync every 20 minutes during game hours.

© 2026 Johnattan García-Ruiz Last updated