If you've used Probo or any "opinion trading" app during an IPL match, you know the experience: the next over hasn't even started and you're buying YES at ₹3 that India will hit a six. Three balls later, your YES is worth ₹7 because the bowler has just been hit for two boundaries. You sell. You make ₹4 in 90 seconds.
This is a real-time prediction market. Underneath the breezy UX is one of the harder engineering problems in consumer fintech. At Xenotix Labs we built the trading engine for Cricket Winner. Here's the architecture.
The model
A market is a binary question that will resolve to YES or NO at a specific moment. "Will India win the toss?". "Will Kohli score a fifty in this innings?". "Will the next ball be a wide?".
Users buy YES or NO contracts. Prices are in rupees and always sum to ₹10 (because exactly one side will pay out ₹10 on resolution). If YES is ₹7, NO is ₹3. As opinion shifts, prices move.
When the market resolves, holders of the winning side get ₹10 each. Holders of the losing side get ₹0.
What's hard
- Order books are real-time. Every buy or sell shifts the price; clients need updates within ~200 ms.
- Settlement is binary and final. When India wins the toss, every YES holder needs ₹10 in their wallet within seconds, deterministically.
- Markets resolve fast. A "next ball" market opens for ~30 seconds. Tens of thousands of orders may flow through in that window.
- Money is involved. No skipped writes. No double-payouts. No drift. Wallet ledgers must reconcile down to the paise.
The pipeline
Client → REST place_order → Order Service → Kafka (trades-topic, partitioned by market_id)
↓
Matching Engine consumer (one per partition)
↓
Order book updates + matched trades
↓
Postgres write + Wallet debit/credit
↓
Redis pub/sub for price updates
↓
WebSocket gateways → Clients
The key constraint: per-market ordering must be strict. If two orders arrive at the same millisecond, only one of them can match the standing best bid; the other goes into the book or matches the next best.
We enforce this by partitioning Kafka by market_id, with one matching-engine consumer per partition. Within a partition, Kafka guarantees total ordering, so the matching engine processes orders one at a time, deterministically.
Why one matching engine per market
A matching engine is a state machine: order book in, trades out. If two engines act on the same market simultaneously, you get races. So we run one engine per market — single-threaded, in-process, with the order book held entirely in memory.
This sounds risky. "In memory" implies "lost on restart." The mitigation: every event is durably written to Kafka before the engine processes it. On restart, the engine replays all events from the beginning of the partition (or from a snapshot) and reconstructs the order book exactly.
We also snapshot the order book every 30 seconds to a Postgres order_book_snapshots table to bound replay time.
The wallet integration
Every trade involves two wallets: the buyer's (debited) and the seller's (credited). Both must update atomically.
We never call the wallet service synchronously from the matching engine. Instead, the engine emits a trade-executed event to another Kafka topic, and a wallet-update worker consumes those events and applies them as immutable rows to the wallet ledger (see our other post on why wallets are ledgers).
If the wallet update fails, the trade row is marked pending_settlement. A reconciliation worker retries every minute until success or hard failure. We've never lost money this way.
Settlement
When a market resolves (the official source says "India won the toss"), an admin endpoint marks the market as settled with the outcome. A settlement worker reads the order book + position table, generates one payout row per holder, and pushes the payouts through the same wallet-update pipeline.
Settlement is also idempotent: every payout is keyed by (market_id, user_id), so reruns don't double-pay.
The prices
Prices in this model are derived from the order book. The "current price" of YES is the midpoint of the best bid and best ask in the YES order book. As the book shifts, the price shifts.
We push price updates to clients via WebSocket every time the midpoint changes (deduped to ~10 Hz max, to avoid flooding mobile clients on volatile markets).
What's hard about real-time UX
The trading screen has to feel instant. The user taps "Buy YES at ₹7" and the price was ₹7 when they tapped. By the time the request reaches the server, it might be ₹7.50.
We handle this with limit orders + slippage protection. The user's request includes the price they saw. If the actual matched price exceeds it by more than the user's chosen slippage tolerance (default 5%), the order is rejected and the user is shown the new price. They re-confirm or back off.
This is how real exchanges handle the same problem. It's table stakes for fairness.
What we'd do differently
- Snapshot more aggressively. 30 seconds is fine; 5 seconds is better. Replay time matters during incident recovery.
- Use a separate Kafka cluster for the trade pipeline. Don't share with general application events. Trade volume is bursty and you don't want it competing for broker resources during match days.
- Pre-warm matching engines for upcoming markets. When a market opens 30 seconds before tipoff, the engine should already be ready, not cold-starting.
- Build a dedicated reconciliation dashboard from day one. When something goes wrong, you need a UI to see exactly which trades didn't settle, why, and a single-click "retry" button.
Stack summary
- Mobile: Flutter
- Web: Next.js
- API gateway: Node.js
- Matching engine: Node.js single-threaded worker per market partition
- Event bus: Kafka, partitioned by market_id
- Real-time: WebSockets + Redis pub/sub
- Wallet: PostgreSQL ledger
- Snapshots / reconciliation: PostgreSQL
- Deployment: AWS MSK + ECS
Building a prediction market or trading product?
Real-time markets are unforgiving — every drift between client price, server price, and settlement value erodes trust. If you're building one, Xenotix Labs has shipped the full stack from Flutter UX to Kafka matching engine to settlement reconciliation. Reach out at https://xenotixlabs.com.
Top comments (0)