Skip to main content

ADR-001: Single Canonical Order Book

Status

Accepted

Context

Binary prediction markets have two outcomes (YES and NO), which naturally suggests two order books - one for YES shares and one for NO shares. However, this design decision has significant implications for:

  1. Liquidity fragmentation - Two books split market makers' capital
  2. Complexity - Users must reason about two separate markets
  3. State size - Two orderbook accounts per market
  4. Matching logic - Cross-book arbitrage opportunities

The Core Insight

In a binary market, buying YES at price p is economically equivalent to selling NO at price 1-p:

Buy YES @ 60% = Pay 0.60 USDC, receive 1 YES share
Sell NO @ 40% = Receive 0.40 USDC, give 1 NO share

If outcome is UP:
- YES buyer: Paid 0.60, receives 1.00 = +0.40 profit
- NO seller: Received 0.40, owes 0.00 = +0.40 profit

The trades are equivalent.

Decision

Implement a Dual-View Single Engine architecture:

  1. Store all orders as YES share orders in a single canonical order book
  2. Convert NO orders to equivalent YES orders before storage
  3. Present users with four intuitive order types (BuyYes, SellYes, BuyNo, SellNo)

Conversion Rules

Buy YES @ p    →  Canonical BID @ p
Sell YES @ p   →  Canonical ASK @ p
Buy NO @ q     →  Canonical ASK @ (10000 - q)
Sell NO @ q    →  Canonical BID @ (10000 - q)

Examples

User OrderUser PriceCanonical SideCanonical Price
Buy YES6000 bpsBID6000 bps
Sell YES6000 bpsASK6000 bps
Buy NO4000 bpsASK6000 bps
Sell NO4000 bpsBID6000 bps

Consequences

Positive

  1. Unified Liquidity

    • All liquidity concentrates in one book
    • Tighter spreads for users
    • Better price discovery
  2. Reduced State

    • One orderbook account instead of two
    • 50% reduction in storage costs
    • Simpler account management
  3. Simpler Matching

    • Single matching engine
    • No cross-book arbitrage needed
    • Deterministic execution
  4. Consistent Pricing

    • YES @ 60% and NO @ 40% are always consistent
    • No price divergence between books
    • Intuitive implied probability
  5. Natural Arbitrage Prevention

    • No opportunity for cross-book arbitrage
    • Prices stay aligned by construction

Negative

  1. Conversion Overhead

    • Extra computation for NO order conversion
    • Users must understand the equivalence
  2. Mental Model Complexity

    • Advanced users see the canonical form
    • May be confusing initially
  3. Tick Rounding Asymmetry

    • NO orders experience inverse tick effects
    • Buy NO @ 4050 → Ask @ 5950 → rounded to 6000

Mitigations

  1. Clear Documentation

    • Explain equivalence in user guides
    • Show both user and canonical views in UI
  2. SDK Abstraction

    • SDK handles conversion automatically
    • Users interact with intuitive order types
  3. Transparent Rounding

    • Show effective price after rounding
    • Warn about tick boundary effects

Alternatives Considered

Alternative 1: Two Separate Order Books

Maintain independent YES and NO order books.

Rejected because:

  • Fragments liquidity
  • Requires cross-book arbitrage mechanism
  • Doubles state requirements
  • More complex matching logic

Alternative 2: Combined YES/NO Shares

Use a single share type that can be positive (YES) or negative (NO).

Rejected because:

  • Negative balances are confusing
  • Harder to reason about solvency
  • More complex accounting

Alternative 3: AMM Instead of Order Book

Use automated market maker (Uniswap-style) for pricing.

Rejected because:

  • Impermanent loss for LPs
  • Worse execution for large orders
  • Less capital efficient for binary outcomes
  • Price manipulation easier

Implementation Notes

Conversion Function

pub fn convert_to_canonical(
    side: UserOrderSide,
    price_bps: u16,
) -> (CanonicalSide, u16) {
    const P_MAX: u16 = 10_000;

    match side {
        UserOrderSide::BuyYes => (CanonicalSide::Bid, price_bps),
        UserOrderSide::SellYes => (CanonicalSide::Ask, price_bps),
        UserOrderSide::BuyNo => (CanonicalSide::Ask, P_MAX.saturating_sub(price_bps)),
        UserOrderSide::SellNo => (CanonicalSide::Bid, P_MAX.saturating_sub(price_bps)),
    }
}

Invariant

After conversion, the standard order book invariant holds:

best_bid < best_ask (when both exist)

This is equivalent to:

best_yes_bid < best_yes_ask
best_no_bid < best_no_ask
best_yes_bid + best_no_ask <= P_MAX
best_no_bid + best_yes_ask <= P_MAX

References

  • ORDERBOOK.md - Full order book specification
  • Binary prediction market theory
  • Polymarket's dual-book design (contrast)

Changelog

  • 2026-01: Initial decision