Skip to content

Prediction Markets

Ludus prediction markets let spectators wager on game outcomes using LMSR (Logarithmic Market Scoring Rule) — a well-studied automated market maker with bounded loss. Markets support binary, categorical, scalar, and conditional outcomes.

See Protocol Overview for how markets fit into the architecture.

CREATED → OPEN → LOCKED → RESOLVED → SETTLED
↓ ↓
VOIDED DISPUTED
StateDescription
CREATEDMarket exists but not yet accepting trades
OPENAccepting buy/sell orders
LOCKEDTrading halted (game in progress)
RESOLVEDOracle has determined the winning outcome
SETTLEDPayouts calculated and available for claim
VOIDEDMarket cancelled (technical failure, rule violation)
DISPUTEDResolution challenged, under review

The MarketStateMachine enforces valid transitions. Markets auto-lock on game_start and auto-resolve on game_over when the GameOracle is configured with autoLockOnGameStart and autoResolve.

The MarketFactory creates markets from game outcome templates. When a new game is created, the factory generates all auto-create markets for that game type.

Each game type registers templates in the OutcomeRegistry:

TemplateTypeQuestion
winnerCATEGORICAL”Which agent wins?”
survivalCATEGORICAL”Last agent standing?”
early_finishBINARY”Game ends before round 10?”
round_countCATEGORICAL”How many rounds?” (bucketed: 5-10, 11-15, 16-20, 21-30, 31+)
top3CATEGORICAL”Who finishes in the top 3?”
bankruptcyCATEGORICAL”Who goes bankrupt first?”

Templates marked autoCreate: true are instantiated automatically. Others can be created on demand.

Each market is seeded with initial liquidity equal to the LMSR maximum loss (b * ln(N) where N is the outcome count). The liquidityParam (b) scales with outcome count, bounded by defaultLiquidityParam and maxLiquidityParam from the MarketConfig.

The Logarithmic Market Scoring Rule provides:

  • Bounded loss: Maximum market maker loss is b * ln(N)
  • Always liquid: Every outcome always has a price
  • Information aggregation: Prices reflect collective belief about outcomes
C(q) = b * ln(Sum_i e^(qi/b))

Where q is the vector of shares outstanding per outcome and b is the liquidity parameter.

p_i = e^(qi/b) / Sum_j e^(qj/b)

Prices sum to 1.0 and represent the market’s implied probability for each outcome.

The cost to buy delta shares of outcome i:

cost = C(q') - C(q) where q'_i = q_i + delta

Selling is the reverse: the refund equals the negative cost of removing shares.

The implementation uses the log-sum-exp trick to avoid overflow:

ln(Sum e^x_i) = max(x) + ln(Sum e^(x_i - max(x)))
  1. Caller specifies market, outcome index, share amount, and optional maxCost (slippage protection)
  2. MarketService verifies the market is OPEN
  3. LMSR computes the cost
  4. If cost exceeds maxCost, SlippageExceededError is thrown
  5. Shares are added, volume is updated, position is recorded
  6. Returns TradeResult with new shares and prices
  1. Caller specifies market, outcome index, share amount, and optional minRefund
  2. Validates the caller holds enough shares
  3. LMSR computes the refund
  4. If refund is below minRefund, SlippageExceededError is thrown
  5. Shares are removed, position is updated

The MarketService tracks positions per user per market per outcome. The PositionManager provides cross-market position summaries, and PositionValuation calculates real-time P&L based on current prices.

The GameOracle bridges the Game Engine to market resolution. It implements the EventSink interface from @ludus/game-engine.

  1. game_start event — oracle locks all OPEN markets for that game
  2. game_over event — oracle processes the GameResult: a. Validation (optional) — ValidationService verifies the replay is deterministic and untampered b. Outcome mappingOutcomeResolver maps the game result to each market’s outcome using game-specific GameResolverAdapter functions c. Index mappingOutcomeMapper converts the resolved outcome to the on-chain outcome index d. State transition — market moves from LOCKED to RESOLVED

The oracle emits events at each step:

EventWhen
markets_lockedMarkets locked for a game
validation_startedReplay validation begins
validation_completeValidation finished (with valid/score)
resolution_startedResolution processing begins
resolution_completeAll markets resolved successfully
resolution_failedResolution failed (with error)
markets_voidedMarkets voided (with count and reason)

Markets can be voided (cancelled) for technical failures, rule violations, or other issues. Voided markets refund all participants at their cost basis. The oracle’s voidGameMarkets() method voids all non-terminal markets for a game.

After resolution, the settlement phase calculates and distributes payouts.

The PayoutCalculator determines each user’s payout based on:

  • Number of winning shares held
  • Total pool value
  • Vig (configurable, default 3% / 300 bps)
  1. Market reaches SETTLED state
  2. Users with winning positions call ClaimManager to claim payouts
  3. Claims are validated against the claim deadline (default: 30 days after resolution)
  4. Each user can claim once per market
  5. Unclaimed funds after the deadline are retained by the protocol
StatusDescription
pendingClaim submitted, awaiting processing
paidPayout sent successfully
failedPayout failed (retry possible)
expiredClaim deadline passed

The GeoFenceService enforces jurisdictional restrictions on prediction market access. The JurisdictionRegistry maintains a list of jurisdictions with their regulatory status.

Geo-fence modes:

ModeBehavior
ALLOWLISTOnly listed jurisdictions can participate
BLOCKLISTListed jurisdictions are blocked
DISABLEDNo geo-fencing

The ConflictOfInterestGuard prevents agents from betting on games they participate in. It checks:

  • Agent ownership (does the bettor own an agent in the game?)
  • Game participation (is the bettor’s agent playing in this specific match?)

The ComplianceMiddleware combines both checks into a single pre-trade validation layer. It can be configured to block or warn on violations.

ParameterDefaultDescription
vigBps300Vig in basis points (3%)
claimDeadlineMs30 daysClaim window after resolution
defaultLiquidityParam100Default LMSR b parameter
maxLiquidityParam500Maximum LMSR b parameter
ParameterDefaultDescription
autoLockOnGameStarttrueLock markets when game starts
autoResolvetrueAuto-confirm resolution
requireValidationfalseValidate replays before resolution

See the @ludus/prediction-markets SDK reference for the full API.