RLRoman Levchenko
WorkHow it's builtAI workflow
Get in touch
← All work

W02 / Case study

A real-money prediction market, built end to end

Contracts are the source of truth and everything else can be rebuilt from on-chain events, so a small team can run real money safely.

Client
Tennis Expert · own product
Role
Founder and lead engineer: contracts, worker, frontend, infrastructure
Period
2025 — now
Stack
  • Solidity
  • Polygon
  • NestJS
  • BullMQ
  • Supabase
  • Next.js
  • Thirdweb
sync time behind the chain on Polygon
42 s → 6 s
automated tests, including contract invariants
~2,800
languages, with parity checked in CI
12

01 / Context

Context

Tennis Expert is a real-money prediction market for professional tennis, live on Polygon. Fans back the exact set score of a match with USDC in one signature, and everyone who picked the winning score splits the pool, minus a 3% rake. The house never quotes odds, so it never takes price risk. Around that core sit tournament brackets with on-chain escrow, free matches, achievement badges, a leaderboard with prizes, private leagues and a daily quiz.

02 / Problem

Problem

Real money on a chain that can reorganise, run by a very small team. Every write had to be safe to repeat, results had to become final without paying for an oracle, and the off-chain database could never quietly become the source of truth. The first version, which minted an NFT ticket per prediction, also had to give way to a simpler pool model.

03 / Architecture

Architecture

ClientPolygonWorkerDataBrowserNext.js on VercelThirdwebwallet, social loginPool contractsUSDC escrow, settlementSync workerNestJS on Fly.ioJob queueBullMQ on RedisRead cacheSupabase Postgres
  1. Browser — Next.js on Vercel
  2. Thirdweb — wallet, social login
  3. Pool contracts — USDC escrow, settlement
  4. Sync worker — NestJS on Fly.io
  5. Job queue — BullMQ on Redis
  6. Read cache — Supabase Postgres

Money and outcomes live on-chain. The worker replays events into a read cache that can be rebuilt at any time.

04 / Key decisions

Key decisions

  • The database is a cache

    Supabase holds a read model rebuilt by replaying on-chain events. Pages load fast and nothing off-chain can disagree with the contracts.

    Trade-off Data lags the chain slightly, covered by optimistic UI rather than more polling.

  • Payouts are pushed, not claimed

    A permissionless payout function pays winners automatically, and keeps working even while the contract is paused.

    Trade-off The platform pays gas for every winner, so a manual claim stays as a fallback.

  • Propose, then challenge, instead of an oracle

    Results are proposed and open to challenge for at least an hour before anyone can finalise them. A market is never voided automatically for missing data.

    Trade-off Settlement waits for the challenge window.

  • Settlement terms fixed at creation

    The rake is snapshotted when a market opens, voids take no rake, and the rake can never exceed the losers' pool, so no winner gets back less than their stake.

    Trade-off Changing fees only affects new markets.

  • Observability on a budget

    Full APM was turned off on purpose, roughly halving the monitoring bill; custom metrics still drive 35 monitors.

    Trade-off Less automatic tracing; new hot paths need explicit metrics.

05 / Hard problems

Hard problems

  1. Reorgs versus sync speed

    The worker trusts the chain's finalized head only when it is fresher than a fixed confirmation depth. Polygon sync dropped from about 42 s to 6 s, and another chain was saved from a silent 20-minute stall.

  2. Writes that are safe to repeat

    Events are stored under a unique chain, transaction and log key, jobs are de-duplicated by id, and a distributed lock keeps two worker instances from doing the same work.

  3. One-signature bets

    Bets use a USDC permit so there is no separate approval step, with a fallback to the existing allowance when a permit is front-run.

06 / How it was built

How it was built

Agents work inside hard rules: an AGENTS.md with constraints, a 21-principle constitution, a domain glossary, a guard hook that blocks edits to secrets and retired contracts, a security-review agent on every contract change, and a nightly headless run that reviews production logs. 522 commits are co-authored with Claude.

← PreviousFewer LLM calls, better matchesNext →Search you can trust on a live-commerce platform

© 2026 Roman Levchenko · Warsaw, EU

Built with Next.js, NestJS and Postgres on Railway. See how it's built

  • LinkedIn
  • GitHub
  • Upwork