A real-time World Cup prediction game I designed, built, and ran solo, then took apart with data.
My Role
Solo, end-to-end: concept, product design, architecture, AI-directed build, live operation, and post-mortem analysis.
Overview
A multiplayer prediction game for the FIFA World Cup 2026: friends create rooms, predict scores, group standings, and special bets on a leaderboard that updates live during matches. I built it solo in about six days of focused work, spread across the month before kickoff, and ran it live for six weeks, patching a failing data API on day one, shipping new features mid-tournament, and settling disputes with an audit trail I'd designed in from the start. I kept it deliberately small: a controlled test of a real-time system, measured honestly and analyzed for a v2. The story isn't scale, it's the judgment that keeps a one-person live product fair and standing under pressure.
Results
~6 days
Of hands-on build time
4
Features shipped mid-tournament
1,764
Predictions logged, about 77 per active player
61%
Of active players came back on 5+ separate days
Overview
I designed, built, and shipped a real-time product on my own in about six days of focused work, spread across the month before kickoff, then ran it live for six weeks while a World Cup happened around it. Over those weeks I patched a live API, added features mid-tournament, and resolved disputes with data.
I could see the idea had real commercial potential, but before betting on that I wanted to validate it on two fronts at once: the technical one, whether a single person could build and run a reliable real-time system like this, and the conceptual one, whether the format actually worked and people would find it fun enough to keep playing. So I treated the World Cup as a controlled product test: real players and real stakes, deliberately contained, with no marketing push, because the goal was to prove the idea, not to grow it yet. The interesting part of the project is what that test surfaced: the decisions that kept a one-person, hot-patched, live product fair and functional under pressure, and the data to point a commercial v2 in the right direction.
By the numbers
~6 days
Of hands-on build time
4
Features shipped mid-tournament, live
104
Matches tracked in real time
1,764
Predictions logged, about 77 per active player
61%
Of active players came back on 5+ separate days
01 — Where It Came From
A group of friends, all football obsessives, wanted to bet among ourselves for the World Cup: the champion, the top scorer, the surprise team, exact scores, all of it. We couldn't find an app that let us do it our way, custom, so I decided to build one.
It was for us, but I suspected it had legs. Few things generate as much passion as a World Cup, and soon the application was shared with other friends and people created their own rooms to play. So I set two rules for myself:
→ Ship it, don't market it. Test the real-time system under controlled, real conditions before it ever mattered to strangers.
→ Respect the constraint. I started about a month before kickoff. Solo. Code AI-directed. And with one problem I couldn't design my way around.
The problem that shaped everything: the whole app depended on a live football data API (football-data.org), and there was no way to test it against real matches until the tournament actually started. I was building on top of a data source I couldn't fully trust until it was too late to change course. Nearly every important decision below is a response to that single unknown.
02 — Four Decisions That Carried the Project
This is the part I'm proud of. Under a hard deadline, with code I was directing rather than writing line by line, a few early calls did the heavy lifting for the entire tournament.
1. Treat every user input as sacred
The first rule I set in the data model: anything a user enters is immutable and timestamped. Once a player submits a prediction, it's written to the database and custodied. Never silently overwritten, always with a record of exactly when it arrived.
It turned out to be the decision that let me resolve every dispute the tournament threw at me, fairly and with evidence.
2. A manual override the automation couldn't undo
Because I couldn't trust the API until it was live, I built an admin module that could override any API-generated result, and, critically, a flag (admin_override) that stops the automated sync from reverting a human correction. The cron job and a human could operate on the same data without fighting each other.
This was the safety net. When the API failed on day one, it's the only reason the game kept running.
3. Scope ruthlessly: automate the live experience, handle the rest by hand
The API gave me live scores and finished matches. It did not give me the "global" results players were betting on: final group standings, top scorer, champion, surprise team.
Automating those would have meant building an entire tournament engine that tracked every team's points, goal difference, and position across the group stage. With the clock running and a real-time system to get right, that was the wrong place to spend time. So I made a call: handle global results manually, and put all my effort into the thing that actually had to be flawless, the live in-game experience. Managing real-time state was already more than enough to own.
Prioritization under a deadline isn't glamorous, but it's the decision that made shipping on time possible.
4. No application backend
The architecture is deliberately minimal: React on the front, Supabase as the entire backend, Vercel Cron for the only server-side work there is. There's no custom API layer. Business logic and authorization live in Postgres Row-Level Security; the client talks to Supabase directly. The leaderboard reads in constant time because points are recalculated and cached (pts_cache) on every result update rather than computed per request.
For a small team and a fixed-lifespan product, this erased an entire category of work (no backend to build, deploy, or maintain) and let me spend my time on domain logic and experience instead of plumbing. A commercial version with prizes, money, or real betting would push the sensitive logic behind a server-side layer (Supabase Edge Functions or Postgres functions) rather than leaning on RLS alone; for a contained, no-stakes test, the minimal setup was the point.
03 — The Game
Before any of the live machinery, here's how it actually played.
Getting in
Sign up with email or Google, then pick a name, jersey color, and number. That jersey followed players everywhere (leaderboard, chat, share cards), giving the game personality from the first screen.
Rooms
Create or join rooms via a short unique code (e.g. NSE7U7). No membership cap: 37 players held 45 memberships between them, running parallel private leagues and comparing standings across multiple rooms at once.
Scoring
Matches pay 3 points for the outcome, 6 for the exact scoreline. Global bets locked before kickoff (group standings, champion, top scorer, surprise team) run 2 to 3 points per position, up to 25 for the big calls. Frequent match points keep everyone in it; the high-value globals give anyone a way back.
The leaderboard was designed to move
Global bet points were recalculated matchday to matchday as real standings shifted, handed out and taken back throughout the group stage. One unexpected result could reshuffle the top. That volatility was deliberate: a leaderboard that swings on one match is one people can't stop checking.
04 — Running It Live
The tournament kicked off and, as I half-expected, the API misbehaved. For the first two matchdays I entered every result by hand through the admin module, exactly the fallback I'd built for. By the third matchday I'd sorted it out, and from there the API handled kickoffs, final whistles, and scores on its own, with the manual override sitting quietly in reserve.
Then I started adding features while the tournament ran: boosts, golden games, and Duels (1v1, all detailed in the app).
| Feature | Metric | Verdict |
|---|---|---|
| Match Predictions | 1,764 predictions · 62% of registered players made at least one | Core feature. Solid adoption among active users |
| Boosts | 208 used | Well adopted |
| Golden Games | 70 matches configured (4 rooms) | Successful where enabled |
| Duels (1v1) | 71 created → 48% resolved | Mixed. High drop-off, only enabled in 2 rooms |
| Chat | 0 messages | Unused. Dead feature |
App-wide (22 rooms, 37 unique users)
Product lesson
Some players pushed back on new mechanics landing mid-tournament. Fair enough: it felt like changing the rules mid-game. So I limited new features to rooms of people I knew personally, who could try them and give me honest feedback, while everyone else kept the game they'd started with. A segmented rollout, by necessity.
Engineering lesson
Every new feature touched more of the system than it looked like it would, and the AI I was directing didn't always trace the dependencies. The recurring failure mode: a feature would work in isolation but break a connection somewhere downstream. Duel points, for instance, were saved correctly but didn't surface in the leaderboard. My job stopped being "write the feature" and became "find every place this feature quietly touches," which is most of the real work in a system with this many moving parts.
05 — The 6-1 That Couldn't Be Real
My favorite story from the tournament, because it's where an early decision paid for itself.
The Duels feature introduced a subtle bug. If player A challenged player B and B never accepted or rejected, an edge case left the "enter result" path open past the point where the API was supposed to lock it at kickoff. One player used that gap to enter a 6-1 on a match that was already minutes from full time.
A 6-1 is a wildly improbable scoreline to predict, improbable enough to be suspicious. But suspicion isn't proof. This is where decision #1 did its job: because every input carried a timestamp, I went to the database and saw it plainly. The prediction had been entered after kickoff. Not a lucky guess. A bug being exploited.
I fixed the lock, corrected the score, and handed out the points people had actually earned. No arguments, because I wasn't ruling on a hunch. I had the record. That's the entire reason I made user data immutable and timestamped weeks earlier: not for this bug specifically, but so that whatever went wrong, I could always reconstruct the truth and be fair.
06 — What the Players Taught Me
Running something live means watching real people use it in ways you didn't design for. A few honest ones:
→ People don't read, even when the rules are right there. The rules weren't hidden: a persistent "How to play" button sat at the top, and the instructions modal opened automatically the first time anyone entered a room. People skipped it anyway. Some missed the deadline for global predictions (champion, top scorer, standings), so I reopened the window to let them in. Another submitted every prediction for the entire World Cup before a single match had kicked off, not realizing scores could go in per match right up to each kickoff. The lesson isn't that the rules needed to be clearer, it's that visible and available doesn't mean absorbed. A v2 builds the timing model into the flow itself, learned by doing instead of by reading.
→ The tab split was wrong. Matches started organized into Group Stage and Knockouts tabs. But the World Cup isn't played in group order: each matchday mixes teams from different groups, and hunting across tabs for the next game was friction. A couple of matchdays in, I moved every match onto a single chronological screen. It made the tournament's progress legible and killed the navigation annoyance instantly.
07 — The Post-Mortem
Because I ran it as a controlled test, I owed it a real analysis, reconstructed from direct SQL queries against production and cross-referenced with Google Analytics. The short version: the system worked, and the only real gap was getting people in the door.
What worked
→ It ran itself, across time zones. After the day-one API trouble, the automation carried the whole tournament unattended and in real time. Matches ended while I slept in Spain and players in the Americas still saw correct, live standings, with no one at the controls.
→ The data was reliable. After matchday one, the API fed trustworthy real-time results that underpinned every feature. Beyond a few quickly-fixed bugs, six weeks of clean behavior.
→ Real engagement with zero marketing. No promotion at all, yet 61% of active players came back on five or more separate days, averaging about 77 predictions each. For anyone who got in, it was sticky.
What to fix
→ The top of the funnel. 73% of visitors never made a prediction, dropping off before registration. That's an acquisition and onboarding gap, not the product failing: everyone who got inside played and stayed. It's the number-one v2 priority. Late joiners tell the same story: they felt they'd "missed the start" and never began.
08 — Key Takeaways
→ Immutable, timestamped data isn't overhead, it's leverage. The decision that felt over-cautious for a game among friends turned out to be the only reason every dispute could be settled with evidence instead of authority.
→ A manual override isn't a failure of automation, it's what keeps a live system standing while the automation earns your trust. Building the fallback before I needed it is the only reason the game survived day one.
→ Reacting to a live moment at full speed is the real skill. A World Cup opens a window that closes in six weeks. This was proof I can move inside one: idea to launched product in days, reshaped live while people played, with data read fast enough to point a second version. That's what building products looks like now that AI is in the loop, and it's how I work.