PropBet
Race the Odds - A Real-Time Crash Betting Game Built with Next.js & Vibe Coding, Cash Out Before It Flies Away
PropBet is a fully playable browser crash game - a plane takes off and climbs an ever-steepening multiplier curve that can burst at any instant. Players stake up to two bets per round and race to cash out before it flies away. It ships with 50,000 demo credits, dual bet panels, auto cash-out, and a live crash-history rail - all driven client-side by a seeded round engine, with no backend to arbitrate a single outcome. PropBet is one game inside a shared "Game Engine" platform, and it was built entirely through vibe coding.
The Build Challenge
A crash game looks like nothing more than a single number going up - but the whole experience lives in the last frame before it stops. The multiplier has to climb smoothly at 60fps on any screen, the crash point has to be decided fairly and stay hidden until the exact instant it bursts, and the cash-out has to resolve against the precise multiplier on the frame the player tapped. Two independent bets and their optional auto-cash-out targets all settle inside the same tick - and there is no server to referee any of it. On top of that, PropBet is one game mounted inside a shared game-engine shell, so its entire round loop had to be self-contained and swappable. All of it was built through vibe coding.
"A crash game is a single number going up - but everything that matters happens in the last frame before it stops."
Design Problem
Build a tense aviation "cockpit HUD" - a plane climbing a neon curve, a giant live multiplier, a colour-graded crash-history rail and two bet panels - that reads at a glance and stays playable on both desktop and a phone held in one hand.
Technical Problem
Drive a 60fps rising-multiplier curve and plane animation from a deterministic round engine, resolve manual and auto cash-outs against the exact frame multiplier, and keep the crash point sealed until the moment the plane flies away.
The Constraint
Describe each sub-system to an AI and iterate - the round state machine, the crash RNG, the cash-out resolver, the curve easing - which demands a precise mental model of the timing race, or the generated code silently mis-pays a round.
Tech Stack & Approach
PropBet is a single Next.js (App Router) application in TypeScript, deployed to Vercel as one game inside a shared "Game Engine" platform - each game mounts into a common shell at its own /games/<name>/play route. The key architectural decision is its canvas-and-refs game loop - Game.tsx renders the canvas and both bet panels once, then a requestAnimationFrame loop drives the entire simulation from refs, so it animates at 60fps while React state updates only a few times a second - just what the panels need to show. The engine (engine.ts) seals the crash point the instant a round begins; the loop simply flies the plane toward a number it already knows.
A Naive Crash Clone vs. How PropBet Is Engineered
setIntervalticks the multiplier → jittery curve, timer drift, dropped frames- Crash checked live against React state → stale-closure cash-out bugs
- One bet, no auto cash-out, no history - nothing to hedge with
requestAnimationFrameloop with a fixed-timestep accumulator → smooth 60fps climb- Crash point sealed at take-off; cash-outs resolved against the exact frame multiplier
- Dual bet panels + per-panel auto cash-out + a live crash-history rail
Stack at a Glance
"PropBet is one game inside a shared Game Engine - no database, no server. The round loop runs entirely in the browser."
"The crash point is sampled once at take-off from a real blockchain block - sealed the instant a round begins, and the loop just flies the plane toward a number it already knows."
"The plane, the curve, the clouds and the multiplier are painted every frame on a canvas - React only re-renders the panels a few times a second."
Core Modules
P(crash ≥ x) = (1−edge)/x at a 3% edge; m(t) = e^(0.1·t).requestAnimationFrame drives the whole simulation from refs; React state syncs a few times a second.mulberry32 PRNG → crash point; a server route proxies the chain and falls back to Math.random().Architecture & Game Loop
The Round Life-Cycle
Every round runs a small state machine: place up to two bets and set auto-cash-out targets, the plane takes off, the multiplier climbs the curve, and then it either gets cashed out or the round crashes and the plane flies away. The crucial detail is that the crash point is computed once at take-off. The loop never "decides" mid-flight - each frame it only checks whether the current multiplier has reached the sealed crash point, or crossed any auto-cash-out target. That separation makes the outcome fixed before the first frame plays, which keeps the render loop fully decoupled from the engine.
A round's crash point has no fixed length - most rounds burst low (1.0x-2x), while a rare few rocket past 40x, exactly like the history rail (1.24x · 2.00x · 5.71x · 18.4x · 42.0x). The engine seals that number at take-off from a seeded draw, so the outcome is fixed before the plane leaves the ground - the loop only reveals it.
The Canvas-and-Refs Game Loop
Game.tsx renders the canvas and both bet panels once, then a useEffect starts the requestAnimationFrame loop. The entire simulation - plane, multiplier, parallax clouds, exhaust particles and the other-player bots - lives in refs, and React state is synced only a few times a second to mirror what the panels show: phase, the throttled multiplier, balance and history. So the canvas animates at 60fps without React re-rendering every frame - which also sidesteps the stale-closure bug that would mis-price a cash-out if the player tapped the button at 8.4x while React held a frame-old multiplier in state.
engine.ts
Pure crash math. sampleCrashPoint draws the bust multiplier from P(crash ≥ x) = (1−edge)/x at a 3% house edge; multiplierAt maps flight time to the live multiplier via m(t) = e^(0.1·t). Owns phase timings, bet limits and the 5,000x cap.
Game.tsx
The component and the loop in one. Renders the canvas + panels once, boots the requestAnimationFrame loop, runs the whole simulation from refs, and syncs throttled state - phase, multiplier, balance, history - to the panels.
blockchainRng.ts
Provably-fair seed. Fetches a real block-derived number from /api/crash-seed, derives a per-round seed, and feeds a mulberry32 PRNG whose first output is the uniform the crash formula consumes - with a quiet Math.random() fallback.
sounds.ts
Audio cues wired to the round phase - take-off, the climbing tone, cash-out and the crash - each mutable from the HUD's sound toggle.
Game Systems & Visual Identity
The multiplier climbs an exponential curve - m(t) = e^(0.1·t), so 2x lands around 6.9s and 10x around 23s. A round's crash point is drawn once at take-off from the industry-standard distribution P(crash ≥ x) = (1−edge)/x at a 3% house edge - a 97% long-run RTP, with about 3% of rounds busting instantly at 1.00x and a rare few flying past 40x toward the 5,000x cap. Two independent bet panels let a player hedge: cash one out early at 2x for safety and let the second ride. The whole cycle - bet, climb, cash out or crash, payout - resolves in the browser with a 50,000-credit demo balance and no real money.
// engine.ts - the crash math (deterministic + provably fair)
HOUSE_EDGE = 0.03 // 97% long-run RTP
GROWTH_K = 0.1 // m(t) = e^(k·t): 2x≈6.9s, 10x≈23s, 100x≈46s
MAX_MULTIPLIER = 5000 // hard cap so a round can't run forever
// crash point - sampled once at take-off from the block seed
sampleCrashPoint(rand): // rand = mulberry32(blockSeed)
u = max(rand(), 1e-12)
raw = (1 - HOUSE_EDGE) / u // P(crash ≥ x) = (1-edge)/x
return clamp(floor(raw*100)/100, 1, MAX_MULTIPLIER)
// each animation frame (Game.tsx rAF loop)
m = multiplierAt(elapsedMs) // = e^(GROWTH_K · ms/1000)
if auto-cashout target hit → settle panel(bet × m)
if m >= crashPoint → CRASH // plane flies away, open bets lostThe Betting System - Where Rounds Are Won
PropBet gives the player three levers, and the tension between them is the whole game. All three are resolved by the same loop that draws the curve - so a payout lands on the exact frame its condition is met.
"Place two bets - cash out one at 2x for safety and let the second chase 20x. Each panel settles on its own."
"Set a target multiplier and the engine settles the moment the curve crosses it - no reflexes required."
"1.24x · 2.00x · 5.71x · 18.4x · 42.0x - the rail is the only history the game shows, and every round is independent."
Fairness & the Cash-Out Race
- Each round's seed is a real, public blockchain block fetched via
/api/crash-seed- notMath.random() - That block seeds a
mulberry32PRNG whose first draw sets the crash point, so every round is a reproducible function of a verifiable number - A 3% house edge is baked into the distribution (97% RTP), with a quiet
Math.random()fallback if the chain is down
- Manual or auto - both resolved against the exact frame multiplier
- Win = stake × multiplier at the instant you cash out; both panels settle independently
- Wait too long and the plane flies away - any open bet on that round is lost
PropBet's cockpit-HUD palette - deep midnight-blue instrument panels, electric sky-blue for the live curve and multiplier, altitude amber for the climb, a crash crimson for the moment the plane flies away, and cash-out green for a win.
67, 232, 255
154, 246, 255
244, 198, 74
255, 46, 99
46, 230, 166
11, 18, 32
In-Game Screens
The game across its key states - the branded home screen, the betting board on the airfield, a round crashing mid-air, the round result and settlement, and the how-to-play guide. Click any screen to open it full-size.




















The Vibe-Coding Build Process
Vibe-coding a crash game works only if you own the timing model before you open a prompt. I designed the round state machine, the crash-point distribution and its house edge, the exponential curve, and the cash-out resolution rules on paper first. Only then did I start prompting - describing one sub-system at a time and verifying each against the spec before layering the next. The AI accelerated every implementation step; the spec is what kept a mis-timed cash-out from silently paying the wrong amount.
1 · Design the Math
Specified the round life-cycle (bet → take-off → climb → cash out / crash → payout), the exponential curve, and the seeded crash-point distribution with its house edge - all verified on paper before a single prompt.
2 · Build the Engine
Implemented engine.ts first - the pure crash math: the (1−edge)/x distribution, the e^(0.1·t) growth curve, phase timings and bet limits. Validated the RTP and payouts by hand before touching the UI.
3 · The Canvas Loop
Built the requestAnimationFrame loop inside Game.tsx, driving the whole simulation from refs so the canvas runs at 60fps while React state syncs only a few times a second - no per-frame re-renders, no stale cash-outs.
4 · Cockpit HUD Skin
Translated the type system, the sky-blue-to-crash-crimson palette, the climbing plane path across the parallax skies, and the neon curve into CSS and the canvas render layer.
5 · Polish & Ship
Added the second bet panel, per-panel auto cash-out, the colour-graded crash-history rail, sound, a how-to-play panel and 50,000 demo credits - then mounted it into the shared Game Engine shell on Vercel.
Outcome & Impact
Key Outcomes
"From 1.00x to the moment it flies away - a seeded crash engine, dual bets, auto cash-out and a live history rail - built end-to-end through vibe coding."
📈 Outcome
A shipped, playable browser crash game: a 60fps canvas render loop, a blockchain-seeded provably-fair crash engine, dual bet panels, per-panel auto cash-out, a colour-graded crash-history rail, simulated participants, sound and 50,000 demo credits - all client-side, mounted in a shared Game Engine platform.
🧗 Challenge
The cash-out race is unforgiving - resolve it against a frame-old multiplier and every payout is wrong. Getting it right through vibe coding meant sealing the crash point at take-off and driving the loop with rAF instead of React state, all specced before a single prompt.
💡 What I Learned
The imperative game-loop pattern is the right architecture for a real-time game on React - no state, no reconciliation overhead. The AI produced it correctly only after I specified the pattern explicitly. Design-first vibe coding is the only vibe coding that survives a timing-critical loop.
🚀 Next Steps
Surface the block hash behind each round as an in-game "verify" link, persist balance and history across sessions, replace the simulated participant bots with a real multiplayer socket so bets are shared between players, and grow the shared Game Engine shell with more titles beside PropBet.
