MatchRoom — About the project
Inspiration
Every big match, my group chat turns into chaos. Someone types "he's going to miss" and the text lands after the ball is already in the net. There is energy, but it's disconnected from the game — a message thread can't capture what it feels like to stand among supporters who are celebrating, arguing, and willing their side forward.
I wanted to build the room around the match instead of next to it. A second screen where you pick a side, call the next moment before it happens, and visibly push your faction forward — synchronized to the match clock, with your friends, wherever they are. And I wanted the AI part to not just be a chatbot bolted on. In MatchRoom, an AI producer that watches the event feed and invents prediction moments on the fly, and an AI referee that turns dropped news links into competitive currency.
What it does
MatchRoom is a social second-screen app with two kinds of rooms:
Live match rooms. A replay engine streams a real match (the 2022 World Cup final) as if it were live. An LLM producer watches the event feed and opens time-limited micro-predictions at tense moments — "Mbappé steps up. Does he bury this penalty?" — with a countdown to lock your pick. You choose a faction (home/away), commit limited conviction tokens, react to moments, follow spoiler-free live player stats, and climb a leaderboard alongside AI personas with distinct personalities. Everything is synchronized across browsers through Supabase Realtime: same clock, same cards, same crowd.
Debate rooms. Camp-vs-camp chat around a story (a transfer saga, a GOAT debate). There are no polls here, so the competitive loop runs on receipts: drop a source link and an AI referee fetches the page, grades it on relevance, credibility, and how much it actually supports your camp, and pays points into your faction. A retrieval-grounded AI pundit — The Analyst — reads the room's dropped sources and argues for the opposite camp, citing the domains it leaned on. Anyone can start a 90-second Receipt Battle where both camps race to drop their strongest evidence.
How I built it
The stack is Next.js (App Router) + React + TypeScript, Tailwind for the "Floodlit" broadcast-dark design system, Supabase for Postgres + Realtime, and a thin provider-independent LLM layer (Anthropic / Groq / xAI / Gemini) with smart/fast tiers and hard latency caps.
The architectural principle that everything hangs off is: the LLM authors, the engine settles. The producer writes the question, the options, and a machine-verifiable resolution rule ("resolves YES if a penalty_result event with outcome=scored arrives within 120s") — but only the deterministic replay engine decides winners, by matching events or expiring timeouts. Zod validates every generation; malformed output is dropped and can't-miss moments fall back to hardcoded safety-net cards, so an LLM outage never stalls the room.
The same principle governs the referee. The LLM returns three scores on 0–5 axes — relevance R, credibility C, and camp support S — and the server computes the payout itself:
$$\text{receipt points} = (R + C + S) \times 10 \in [0, 150]$$
The model never touches scoreboard arithmetic, duplicate URLs earn zero, and fetch/LLM failures degrade to a small deterministic fallback grade.
Scoring separates expression from competition. Chat is free and scores nothing; crowd energy counts each member once per moment. Committing k (0, 1, 2, 3) conviction tokens on a correct pick with base stake B pays
$$\text{conviction bonus} = \operatorname{round}(B \cdot k \cdot 0.15)$$
and when the less popular option wins with pick share p, every correct picker earns an underdog bonus:
$$\text{underdog bonus} = \operatorname{round}\big(B \cdot \min(0.5,\; \max(0,\; 0.5 - p))\big)$$
Multiplayer sync is deliberately simple. Rooms store a replay epoch and speed in Postgres, a joining client estimates its clock offset delta from the server timestamp, seeks silently through elapsed events, and advances from the shared wall clock:
$$t_{\text{match}} = \frac{(t_{\text{client}} + \delta) - t_{\text{epoch}}}{\text{ms per match second}}$$
Cards, picks, reactions, member updates, and replay resets all fan out through Supabase Realtime — no custom websocket code. New features (referee verdicts, Analyst replies, Receipt Battle markers) ride the existing reaction stream as encoded labels, so grades and battles reached every client with zero schema changes.
The Analyst is retrieval-grounded without a vector store: the server extracts bounded text from the room's dropped and starter sources through an SSRF-guarded fetch pipeline, stuffs the extracts plus recent chat into context, and the persona pushes back for the opposing camp, citing sources — grounded generation over the room's own corpus, which is exactly the right scale for a room-sized debate.
Challenges I ran into
The spoiler problem. Player profiles originally aggregated stats from the full match timeline — so clicking Messi at kickoff already showed his final goal tally. In a replay-as-live product that's fatal. I rebuilt player profiles to seed rosters from lineups but aggregate stats only from events already revealed by the clock, so the Players tab updates live with the feed and never leaks the future.
LLM latency versus a live clock. A prediction card that arrives after the penalty is worthless. Producer and persona calls run under tight timeouts (~1.5s) with heuristic pre-filters and deterministic fallbacks; slower calls that aren't on the replay clock (source grading, the Analyst) get a separate 8s budget. Designing every AI feature so its failure mode is silence or a fallback, never breakage was the single most repeated lesson.
Realtime consistency. Late joiners need history plus live updates without duplicates, and your own writes echo back through the subscription. I ended up with id-based deduplication everywhere, bootstrap-then-subscribe ordering, and a rule that payouts to yourself apply from the API response while everyone else's arrive via Realtime.
Synchronized minigames without a server tick. The Receipt Battle needed every browser to agree on when a 90-second window ends and who won — without adding server-side resolution. The trick: the battle marker carries its end time in replay-clock seconds, and every client computes the same per-camp totals from the same graded receipts inside the window. Deterministic inputs → identical outcomes on every screen.
Live data temptation. I investigated intercepting a commercial live-scores API and decided against it — signed headers, ToS, brittle. The replay engine emits the same MatchEvent contract a real provider adapter would, so live data is a drop-in change rather than a rewrite.
Accomplishments that we're proud of
- The whole loop works, live, in multiple browsers. Two tabs join the same room, see the same clock, the same producer-generated cards, the same countdowns, and the same settlement — and a late joiner catches up silently to the right minute.
- Zero hallucinated outcomes. Across the entire product, no LLM ever decides who won anything. The producer's cards settle from the event feed, the referee's scores convert to points server-side, and Receipt Battles resolve from deterministic totals. The AI feels alive precisely because it can't cheat.
- Three AI roles that each earn their place. The producer invents verifiable predictions on the fly, the referee turns journalism into a competitive economy, and The Analyst argues from the room's own evidence and cites it. None of them is a generic chat window.
- It degrades, never breaks. Pull the LLM key and you get fallback cards, fallback grades, and a canned pundit line. Pull Supabase and the whole app still runs in solo mode with local scoring. Every external dependency has a rehearsed failure mode.
- Solo, in a weekend, with polish. Full squads with spoiler-free live stats, a broadcast-dark design system, momentum meters, animated settlements — shipped alongside the systems work, not instead of it.
What I learned
- Determinism is the product. Every "wow" moment (AI invents a prediction, referee pays your receipt) is trustworthy only because a boring deterministic layer settles it. Splitting authorship from settlement made hallucination structurally impossible.
- Design the failure mode first. Timeouts, fallback cards, fallback grades, canned Analyst lines — deciding what happens when the LLM doesn't answer shaped the architecture more than the happy path did.
- Transport reuse beats new infrastructure. Encoding new message kinds into one existing Realtime stream let me ship a referee, a grounded debater, and a synced minigame in a weekend without a single migration.
- Incentive design is real design. "Chat scores nothing, unique reactions score once, conviction is scarce, contrarians get paid" took as much iteration as any code — and it's what makes the room feel competitive instead of spammy.
What's next for MatchRoom
Real live fixtures behind the existing MatchEvent adapter, authentication binding members to accounts, rate limits and atomic Postgres scoring functions, persisted custom rooms with invites, and more sports — the event contract and resolution rules generalize well beyond football.
Built With
- ai
- claude
- framer-motion
- groq
- llm
- nextjs
- node.js
- postgresql
- rag
- react
- supabase
- tailwindcss
- typescript
- zod


Log in or sign up for Devpost to join the conversation.