Pick Your Side: I Built a Machine That Manufactures a World Cup Team, Grounded in Real History
Every World Cup app is built for the people who already care. This one is for everybody else: name any two nations and it reads their real history, picks the side you were always meant to love, and a stadium announcer swears you in. The whole architecture bends around one rule: none of the history is invented. Gemini researches with Google Search grounding on, then rewrites into a strict responseSchema, then ElevenLabs performs it over a stadium crowd that ElevenLabs also generated.
The prompt for the challenge was passion, so I took the most literal possible run at it. Every World Cup app ever built is for people who already care: fixtures, brackets, fantasy teams. Every one of them assumes you turned up with a side. A few billion people will watch this tournament and the overwhelming majority of them have no dog in any of it. Pick Your Side is for them.
You name any two nations. It reads their actual history, picks the one you were always meant to love, tells you why, and a stadium announcer swears you in. Two nations in. One side out, and a reason to mean it.

The design constraint: manufactured passion only works if the source is true
The interesting problem is that you cannot fake this with a nice adjective. If the app tells you Ghana and Uruguay have a bitter history and the history is invented, then the passion it hands you is counterfeit, and the moment you look it up you feel stupid and never open it again. So the whole architecture bends around one rule: none of the history is invented.
Two Gemini calls (because one won't do)
- Call 1 has Google Search grounding on. Gemini goes and finds the actual head-to-head record, the actual famous match, and where both sides stand in the tournament being played right now. It comes back with the pages it read and the searches it ran, and the app shows you both. Ask it about Morocco and France and it tells you about the 2026 quarter-final, a result that happened days ago and that no LLM could know from training.
- Call 2 takes those researched facts and nothing else, and is held to a responseSchema so the output is always the same shape: side, verdict, history, stakes, one legendary moment, your reason, a share line, and an announcer script. It's told to pick the side with the deeper wound, not the more famous name.
These two things cannot be combined into one call. Asking gemini-2.5-flash for Google Search grounding and a responseSchema at the same time does not throw, it quietly ignores the schema and hands back prose, which is a fun thing to discover in a UI that expects JSON. So: research first, then shape.


ElevenLabs performs it (and generates the stadium too)
Gemini writes the swear-in and ElevenLabs is the voice that delivers it, over a stadium that ElevenLabs also generated: a restless crowd under the scouting screen while it thinks, and an eruption the instant the nation's name lands. The crowd sits below the announcer and never competes with him. Sound is on by default, because a muted reveal is just a poster, and the control to turn it off is on screen the whole time.
- Live announcer: eleven_flash_v2_5 (returns in under a second so the voice can land on the beat).
- Pre-rendered marquee reveals: eleven_v3 (slower but expressive, understands audio tags, and latency doesn't exist when you generate them offline).
- Stadium ambience: generated by ElevenLabs too. Restless during scouting, erupts on reveal.

About the cache
The Gemini free tier allows roughly ten calls a minute, and every reveal costs two of them. So the marquee matchups are generated ahead of time by scripts/seed-reveals.mjs and committed to data/reveals.json. Those answer instantly and cost nothing, which is what stops a link that travels from handing half its visitors a rate-limit error instead of a team. The seeded reveals come out of the same grounded pipeline as the live ones (real searches, real sources): they are cached, not faked. Any matchup that isn't in the seed is generated live, then held in memory for six hours after that. The tournament is still being played, so a stale 'what is at stake' is worse than a slow one.
The details that matter more than they should
- Nations, their colours, and their nicknames are curated in lib/teams.ts. The model only ever sees a validated id, never raw user text.
- Both routes are rate-limited per IP and carry a global daily ceiling, so a free-tier key cannot be drained by a link that travels.
- Flags are images, not emoji. Windows does not render flag emoji at all (it falls back to letter pairs), and a broken flag is worse than no flag.
- Motion respects prefers-reduced-motion. The stadium eruption becomes a fade for viewers who ask for it.

Pick Your Side: I Built a Machine That Manufactures a World Cup Team for You, Grounded in Real Football History
View the project