The problem
Fantasy games are either single-sport or built around a database rather than a decision. The promise here is narrower and more fun: one spin decides who you can pick, so every draft is a constrained choice rather than a shopping trip, and the simulation is honest enough that the same seed always replays the same season.
What it does
Two reels land on a real club or nation and one of its eligible seasons; that historical squad becomes your pool, and you pick exactly one player before spinning again. A seeded engine then simulates a full campaign — 38, 82 or 162 games depending on the sport — in seconds, and packs the result into a shareable URL. Football, World Cup, NBA, NFL, NHL and MLB, with a daily challenge on an identical seed for everyone.
How it's built
Deterministic simulation, and the bug that wasn’t obvious
A Park-Miller generator keyed on seed plus lineup means the same squad always replays the same season, enforced by tests. Hashing consecutive dates, though, produced near-adjacent seeds whose first output barely moved — so the daily draw froze on the same team for days. It needed a Murmur3 avalanche mix between the hash and the generator.
Share URLs that treat their own payload as hostile
Results are DEFLATE-compressed and base64url-encoded into the URL, taking a full XI from ~1660 to ~450 characters, behind a sentinel byte so older links still decode. The decoder validates every field, enforces one canonical alphabet across Node and browser, and caps inflation to block a decompression bomb.
Fire-and-forget rendering after a hard timeout
Video renders used to be awaited. On one run that hit 305 seconds, Vercel hard-killed the function mid-await and the row was stranded mid-render with the asset already uploaded. Now the worker acks 202, renders, and posts back an HMAC-signed completion verified with a constant-time compare; a row is written before dispatch so a lost webhook is recoverable, and a reaper catches worker crashes.
Publish gates that fail in a chosen direction
Instagram media cannot be deleted via the API, so a bad post is permanent and correctness has to be enforced before publish. The last gate is deliberately regex-based, not model-based, because a model can fail open under load; a vision QC pass over generated cutouts fails closed for generated images but open for real photos, so a provider outage degrades one frame rather than blanking every reel.
Shaping crons around how Postgres bills
The database bills a five-minute minimum per wake, so the cost of a cron is set by how often it wakes the compute, not how much it does. A single Redis key holding the earliest pending timestamp lets a frequent tick prove there is no work without touching Postgres, and related crons are deliberately un-staggered onto shared minutes so they share one wake.
The hard part
Cross-host reproducibility was the one I did not see coming. The audio loudness filter resamples internally, and with no rate pinned, one ffmpeg version on the production box emitted 96kHz while a newer one on my Mac emitted 48kHz — so locally-approved output differed from what shipped. Pinned, with a test guarding it. Asset identity was a genuine modelling error too: deriving the storage key from slug plus team plus version made the key simultaneously the identity, the cache key and the location, so re-rolls silently overwrote their predecessors and there was no way to ask "do we already have this?" without listing the bucket.
Stack
- Next.js
- React Native
- Expo
- Firebase Cloud Messaging
- Prisma
- Neon
- RevenueCat
- PostHog
Built, shipped and maintained by me. Try Go Unbeaten, or see the rest of the work.
