Pony Up Virtual Horse Racing

Pony Up is a free-to-play, real-money-free social simulation game where every player starts each Monday with a $1,000 virtual bankroll.
Pony Up — Case Study
A multiplayer simulated horse racing and casino game built with Next.js 15, Socket.io, and Prisma/Postgres.
Overview
Pony Up is a free-to-play, real-money-free social simulation game where every player starts each Monday with a $1,000 virtual bankroll. The core loop is simple: bet on horse races, grow your weekly stack, and climb the leaderboard. But under that accessible surface lives a surprisingly deep game — horse ownership, stables, breeding, player-owned race tracks, a provably fair blackjack casino, syndicates, auctions, and a persistent Hall of Fame.
The design philosophy is deliberate: reset each week so skill matters more than time invested, keep the economy tight enough that decisions carry weight, and build enough meta-game that dedicated players always have something new to pursue.
The Problem
Most browser-based betting games fall into one of two failure modes: they're either too shallow (tap a button, watch a number go up) or they require real money and carry all the legal and ethical baggage that comes with it. The goal with Pony Up was to find the space in between — a game that feels meaningfully strategic without any financial risk, with a social layer that makes losing interesting and winning satisfying.
The technical challenge that followed was building a server-authoritative, real-time simulation where:
- All players see the same race simultaneously
- Outcomes are random but auditable (no house advantage, no manipulation)
- The economy is fair — a top-up that prevents someone from going broke shouldn't inflate their weekly rank
- The system degrades gracefully (a crashed scheduler shouldn't leave races stuck forever)
Core Game Loop
1. The Weekly Reset
Every Monday, all wallets reset to $1,000. Weekly profit is calculated as current balance − total top-ups received, so players who received pity top-ups don't rank artificially high. This creates a genuinely flat starting line each week.
2. Betting
Players can bet on any upcoming race across a full market:
| Market | Description | |--------|-------------| | Win / Place / Show | Finish 1st, top 2, or top 3 | | Exacta | Predict 1st and 2nd in order | | Trifecta | Predict top 3 in order | | Prop markets | Photo finish, favourite wins, wire-to-wire, stewards' inquiry | | Confidence multiplier | Commit a higher-risk prediction for a bigger payout | | Multi-race tickets | Daily Double, Pick 3/4/6 across consecutive races | | Accumulators | "Let it ride" parlay builder | | Syndicate pools | Bet collectively with other players |
Tipster picks are generated daily using OpenAI and visible to all players. Bets that match a tipster pick receive an additional multiplier if the pick wins.
3. Racing
Races run continuously — every 90 seconds in development, scheduled via cron in production. Each race is:
- Server-simulated: the outcome is computed server-side using a seeded PRNG
- Streamed live: clients receive tick-by-tick position updates over Socket.io
- Auditable: anyone can replay the exact race using the published seed (see Provably Fair below)
Tracks have going conditions (FIRM / GOOD / SOFT / HEAVY) that affect horse performance, and races can be flagged for photo finishes or disqualifications post-result.
4. Progression
Beyond the weekly leaderboard, players can pursue deeper goals:
- Horse ownership: buy, breed, and race your own horses. Horses have ratings, fatigue mechanics, lineage, and can produce foals.
- Stables: purchase stable slots, pay weekly upkeep, hire trainers. Stables gate entry to the horse marketplace.
- Player-owned tracks: buy a race track, upgrade staff, grandstands, drainage, VIP facilities, and casino tables. Tracks earn royalties on every bet placed at them.
- Quests & challenges: daily and weekly objectives that reward consistent play
- Last Man Standing: a separate contest format with its own season structure
- Flash leaderboards: short-window ROI sprints
- Rivalry bets: direct jockey-vs-jockey propositions between players
The Casino
A full blackjack implementation lives alongside the racing game, with its own P&L tracking and Hall of Fame categories. The game includes:
- Standard rules: hit, stand, double down, split, surrender, insurance
- Perfect Pairs and 21+3 side bets
- Provably fair shuffling (see below)
- Session statistics and casino P&L tracked on the player profile
The betting interface uses custom SVG poker chips rendered entirely in React — each chip has a coloured body, alternating edge notches, an inner decorative ring, and a denomination label, designed to feel like sitting at a real table.
Provably Fair Randomness
Both the racing and blackjack systems use a commit–reveal scheme so players can verify that outcomes were not manipulated after bets were placed.
How it works
Before dealing / before race:
serverSeed = randomBytes(32) — generated server-side, kept secret
commitment = SHA256(serverSeed) — published immediately (before any card is dealt)
clientSeed = crypto.randomUUID() — generated in the browser
After the hand / race:
finalSeed = HMAC-SHA256(serverSeed, clientSeed)
deck / race simulation = seededShuffle(finalSeed) — deterministic, replayable
serverSeed is then revealed. Players can verify:
SHA256(revealed serverSeed) === commitment ✓
Because the server commits to a seed hash before knowing the client seed, and the client seed is generated in the browser, neither party can predict or manipulate the final shuffle. A public audit endpoint (/api/blackjack/[handId]/verify and /api/races/[raceId]/verify) returns all seeds for settled hands so players can replay every card or every race tick themselves.
The shuffle uses a Mulberry32 PRNG with a Fisher-Yates shuffle — fast, deterministic, and auditable with a simple script.
Real-Time Architecture
The biggest engineering constraint was keeping a single Node process responsible for both the Next.js HTTP server and the Socket.io WebSocket server. This avoids sticky sessions, shared state across pods, and deployment complexity.
┌─────────────────────────────┐
│ server.ts │
│ ┌──────────┐ ┌──────────┐ │
Client ─── HTTP ────►│ │ Next.js │ │ Socket.io│ │
Client ─── WS ────►│ │ handler │ │ server │ │
│ └──────────┘ └──────────┘ │
│ │ │ │
│ └──── Prisma ──┘ │
└─────────────────────────────┘
Socket.io events drive:
- Race ticks: position snapshots streamed every ~100ms during a live race, including the deterministic RNG state so clients can replay forward
- Race finish: final results, payouts, and leaderboard delta
- Bet activity feed: anonymised recent bets shown site-wide
- Big bet alerts: notable wagers broadcast to all connected clients
- Wallet updates: balance changes pushed immediately after settlements
- Chat: room-scoped messages
- Admin presence: staff-facing online indicators
The race scheduler runs inside the server process and self-heals: if it detects a race stuck in LIVE status, it resumes or cancels it. A cron endpoint (/api/cron/race-scheduler) is also registered as a Coolify scheduled task for defense-in-depth if the in-process scheduler stalls.
Economy Design
Several decisions keep the economy honest:
Adjusted profit ranking — Weekly rank is balance − total top-ups received. Players who hit zero and got a pity top-up don't rank above players who managed their bankroll.
Track royalties — Track owners earn a percentage of every bet placed at their track. Tracks require ongoing investment (weekly bills, upgrades) to remain competitive, creating a player-run economy layer.
Loans with interest — Players can take loans to stay in action, but compound interest means carrying debt into the next week is punishing.
Stable costs — Owning horses isn't free. Weekly stable fees mean a losing stable is a drag on your weekly P&L, not just a vanity feature.
Breeding gates — The horse marketplace is only accessible once a player crosses a cumulative profit threshold. New players bet first, own later.
Hall of Fame
A persistent Hall of Fame tracks all-time records across ~20 categories:
| Category | Description | |----------|-------------| | Biggest single payout | Largest win from a single settled bet | | Biggest trifecta | Largest trifecta payout | | Biggest exacta | Largest exacta payout | | Biggest weekly profit | Most earned in a single week | | Highest all-time profit | Cumulative across all resets | | Longest win streak | Consecutive profitable races | | Most races bet | Volume king | | Most blackjacks | Casino-specific | | Biggest royalties earned | Track owner total | | Pick Six winner | Multi-race ticket king |
Records are stored in a HofRecord table keyed by category, with the holder's user ID and value. Each category updates whenever a new record is set.
Tech Stack
| Layer | Technology |
|-------|-----------|
| Framework | Next.js 15 (App Router) |
| Runtime | Node 22 (custom server.ts) |
| Database | PostgreSQL via Prisma 6 |
| Auth | Auth.js v5 — JWT sessions, Resend magic link |
| Real-time | Socket.io (same process as Next.js) |
| UI | Tailwind CSS, Framer Motion |
| Validation | Zod |
| AI | OpenAI (tipster script generation, TTS commentary) |
| Confetti | canvas-confetti |
| Toasts | Sonner |
| Deployment | Coolify + nixpacks (Node 22 + Postgres) |
The schema has grown to ~40 Prisma models covering auth, economy, racing, betting, ownership, tracks, casino, social, and progression — all in a single Postgres database with standard relational joins.
Audio
The race page plays ambient crowd noise that adapts to the race phase — quieter during the waiting period, louder when the race goes live, fading out when the result comes in. A separate admin-controlled AudioClip system lets staff schedule sounds timed to race events (bugle calls, fanfares). Race commentary scripts are generated by OpenAI and converted to speech, either via the OpenAI TTS API or an optional self-hosted Gradio/Applio endpoint.
Blackjack has its own sound effects driven by the Web Audio API. All audio states persist in localStorage so mute preferences survive page reloads.
Deployment
Production runs on Coolify using a committed nixpacks.toml. On every deploy:
prisma migrate deploy— applies any pending migrations (advisory lock prevents concurrent replicas racing)tsx prisma/seed.ts— idempotent upsert of the canonical horse and jockey poolexec tsx server.ts— starts the combined Next.js + Socket.io server;execmeansSIGTERMfrom Coolify reaches the process directly for clean shutdown
Cron jobs are registered as Coolify scheduled tasks:
POST /api/cron/rollover— Monday 00:05, resets all walletsPOST /api/cron/race-scheduler— every minute, defense against scheduler stallPOST /api/cron/daily-*— horse generation, quest refresh, bills, loan interest, LMS resolution, weekly summary emails
What's Interesting
A few things that aren't obvious from the outside:
Single-process full-stack — One tsx server.ts handles everything: Next.js SSR, API routes, static files, WebSocket connections, and the race scheduler. No separate WebSocket server, no Redis pub/sub, no shared state service. This keeps infra simple at the cost of horizontal scalability (acceptable given the weekly-reset, low-latency-optional design).
Deterministic client simulation — Clients don't just receive final positions; they receive the RNG state at each tick so they can simulate the race forward themselves. The animation is locally driven but stays in sync because the PRNG state is authoritative.
Provably fair in both games — Both the race simulator and the blackjack dealer use the same commit–reveal + HMAC pattern with public verification endpoints. Either game's outcome can be audited with a few lines of Node.
Crash resilience — server.ts registers unhandledRejection and uncaughtException handlers so a stray async error in a non-critical path doesn't kill the process. The race scheduler explicitly checks for stuck LIVE races on startup and resumes or cancels them.
Numbers at a Glance
| Metric | Value | |--------|-------| | Prisma models | ~40 | | Bet market types | 10+ | | Leaderboard categories | 4 (weekly, HOF, tracks, royalties) | | HOF tracked records | ~20 categories | | Cron jobs | 8 | | Socket.io event types | ~20 | | Supported odds formats | Decimal, Fractional, American |
Built with Next.js 15 · Prisma 6 · Socket.io · Tailwind CSS · Auth.js v5 · Deployed on Coolify
Have a similar project in mind?
Talk to us →