Chapter 7: Place Your Bets
How Prediction Markets Work in Ludus
Section titled “How Prediction Markets Work in Ludus”Spectators (and merchants themselves) can bet on game outcomes using LMSR-powered prediction markets. The flow:
Game Created → Markets Created → Markets Opened → Trading → Game Starts → Markets Locked → Game Ends → Markets Resolved → Claims PaidThe GameOracle bridges the game engine to the markets — it listens for game_start (locks markets) and game_over (resolves markets) events.
Three Markets for Mercante
Section titled “Three Markets for Mercante”We define three market types that showcase different outcome structures:
1. Winner Market (Categorical)
Section titled “1. Winner Market (Categorical)”“Which merchant will amass the greatest fortune?”
One outcome per player. The most natural bet.
export function createWinnerMarket( gameId: string, players: Player[], liquidityParam: number = 50,): Omit<Market, "id" | "createdAt"> { const outcomes = players.map(p => ({ id: `winner-${p.id}`, label: `${p.name} wins`, metadata: { playerId: p.id }, }));
return { gameId, gameType: "mercante", template: "mercante-winner", question: "Which merchant will amass the greatest fortune?", outcomes, state: "CREATED", liquidityParam, seededLiquidity: liquidityParam * Math.log(players.length), totalVolume: 0, shares: new Array(players.length).fill(0), lockTime: 0, };}2. Gold Price Market (Binary)
Section titled “2. Gold Price Market (Binary)”“Will Gold exceed 60 florins by game end?”
A simple yes/no bet on price action. Gold is the most volatile good — perfect for binary markets.
export function createGoldPriceMarket( gameId: string, threshold: number = 60,): Omit<Market, "id" | "createdAt"> { return { gameId, gameType: "mercante", template: "mercante-gold-above", question: `Will Gold exceed ${threshold} florins by game end?`, outcomes: [ { id: "yes", label: `Gold > ${threshold}` }, { id: "no", label: `Gold <= ${threshold}` }, ], state: "CREATED", liquidityParam: 30, seededLiquidity: 30 * Math.log(2), totalVolume: 0, shares: [0, 0], lockTime: 0, };}3. Score Range Market (Scalar Buckets)
Section titled “3. Score Range Market (Scalar Buckets)”“What will the winner’s final portfolio value be?”
Four buckets: Under 150, 150-250, 250-400, Over 400 florins.
export function createScoreRangeMarket(gameId: string) { return { // ... outcomes: [ { id: "under-150", label: "Under 150 florins" }, { id: "150-250", label: "150-250 florins" }, { id: "250-400", label: "250-400 florins" }, { id: "over-400", label: "Over 400 florins" }, ], shares: [0, 0, 0, 0], };}LMSR Pricing
Section titled “LMSR Pricing”Ludus uses the Logarithmic Market Scoring Rule (LMSR) for automated market making. It’s the same algorithm used by Polymarket and Augur.
Cost function: C(q) = b * ln(sum of e^(qi/b))Price for outcome i: pi = e^(qi/b) / sum of e^(qj/b)Where b is the liquidity parameter (higher = more liquid, lower slippage) and q is the share vector.
In practice: The MarketService handles all the math. You just call buy() and sell():
import { MarketService } from "@ludus/prediction-markets";
const service = new MarketService(store);
// Spectator buys 10 shares of "Cosimo wins"const trade = await service.buy( marketId, "spectator-123", 0, // outcome index (Cosimo) 10, // shares undefined, // no max cost limit);
console.log(trade.cost); // How many florins it costconsole.log(trade.newPrices); // Updated probabilities for all outcomesMarket Lifecycle
Section titled “Market Lifecycle”CREATED → OPEN → LOCKED → RESOLVED → SETTLED | | +→ VOIDED ←+→ DISPUTED| State | What Happens | Trading? |
|---|---|---|
CREATED | Market exists, not yet active | No |
OPEN | Spectators can buy/sell shares | Yes |
LOCKED | Game started — no more trading | No |
RESOLVED | Game ended — winning outcome determined | No |
SETTLED | All claims paid out | No |
VOIDED | Market cancelled (refund all) | No |
DISPUTED | Resolution challenged | No |
The GameOracle
Section titled “The GameOracle”The GameOracle is an EventSink that bridges game events to market operations:
import { GameOracle } from "@ludus/prediction-markets";
const oracle = new GameOracle(marketService);
// Subscribe to game eventseventEmitter.addSink(oracle);
// When game_start fires:// → oracle.lockMarketsForGame(gameId)// → all OPEN markets for this game → LOCKED
// When game_over fires with GameResult:// → oracle.resolveGame(gameId, gameResult)// → determines winning outcomes// → all LOCKED markets → RESOLVEDFor Mercante, the oracle resolves markets like this:
- Winner market: Compares final rankings — top player wins
- Gold price market: Checks final Gold price vs. threshold
- Score range market: Maps winner’s score to a bucket
export function resolveScoreRange(winnerScore: number): string { if (winnerScore < 150) return "under-150"; if (winnerScore < 250) return "150-250"; if (winnerScore < 400) return "250-400"; return "over-400";}In-Game Betting
Section titled “In-Game Betting”Mercante also allows merchants themselves to bet (the bet action type). This creates an interesting strategic layer:
- Betting on yourself is a confidence play — you lose florins now but win double if you’re right
- Betting on an opponent is a hedge — if they win, your bet softens the loss
- The prediction market reflects collective intelligence — even the AI agents’ bets are informative
Claims and Payouts
Section titled “Claims and Payouts”After resolution, winners claim their payouts:
import { ClaimManager } from "@ludus/prediction-markets";
const claims = new ClaimManager(store);
// Check if a user can claimconst canClaim = await claims.canClaim("spectator-123", market);
// Claim winningsif (canClaim) { const claim = await claims.claim("spectator-123", market, userShares); // claim.netPayout: winnings after 3% vig (house fee)}Testing
Section titled “Testing”describe("Winner Market", () => { it("creates one outcome per player", () => { const market = createWinnerMarket("game-1", players); expect(market.outcomes).toHaveLength(3); expect(market.outcomes[0].label).toBe("Cosimo wins"); });});
describe("Score Range Resolution", () => { it("maps scores to correct buckets", () => { expect(resolveScoreRange(100)).toBe("under-150"); expect(resolveScoreRange(200)).toBe("150-250"); expect(resolveScoreRange(300)).toBe("250-400"); expect(resolveScoreRange(500)).toBe("over-400"); });});What You Have Now
Section titled “What You Have Now”Your game now has a complete prediction market layer:
- Three market types (categorical, binary, scalar)
- LMSR pricing (automated market maker)
- GameOracle that bridges events to market resolution
- In-game betting as a strategic action
- Claim system with deadline and vig
Spectators can watch the game AND trade on outcomes — the prices update live as the game progresses.
Next: Chapter 8 — Ship It