{s}omniamarkets

Market Making on Somnia Markets

What is different about quoting this venue. Somnia Markets runs spot order books, binary prediction markets, and perps, all on the same on-chain order-book engine; this guide covers quoting the binary prediction markets. It assumes you already run market-making systems and skips the generalities — only the mechanics that will surprise you or cost you money if learned in production. Everything below is specific to binary markets (marketType() == BINARY_SINGLE_BOOK), which is every prediction market live today; future kinds (multi-outcome, dual-book) carry a different marketType() and will not share these semantics — branch on the tag, never assume. Full contract mechanics are in Raw Integration; the SDK (@somnia-chain/markets-sdk) wraps all of it if you're in a JS runtime.

One book per binary market; userData selects the side, and NO is price-inverted

Each binary market runs a single order book crossing all four order kinds. userData is not a client tag field — it selects the book half (0 = YES, 1 = NO; anything else reverts InvalidUserData), so tag quote generations off orderId (unique per pool) instead. NO orders are submitted at the YES complement: price = oneCollateral − noPrice, with oneCollateral = 10 ** collateral.decimals(). All prices must sit strictly inside (0, oneCollateral), tick/lot/min from getOrderBookParameters() at runtime.

The kind matrix — note the inversion means isBid does not mean "buy" on the NO book:

userDataisBid = trueisBid = false
0BUY_YESSELL_YES
1SELL_NOBUY_NO

You can quote two-sided with zero inventory

A resting BUY_YES at p and BUY_NO at 1 − p is a full two-sided quote: when either crosses an incoming opposite buyer the pool mints a fresh pair (MINT_A_PAIR) — collateral in, one side to each buyer, no inventory required. SELL_YES × SELL_NO burns a pair symmetrically. This changes the standard prediction-market bootstrap: you never need to pre-mint outcome tokens to open a market, and your quoting book can stay entirely collateral-denominated. Deliberate inventory moves go through mintCompleteSet / mergeCompleteSet (1 collateral ⇄ 1 YES + 1 NO, always), and redeem pays the winning side 1:1 at resolution, less the venue's frozen settlement fee if it configured one (0.5 each on void, never fee'd).

Price the venue's fees into your spread: maker/taker rates are per-venue and frozen into the pool at market creation — read them once per market from getBinaryPoolParams() (makerFeeBpsTimes1k / takerFeeBpsTimes1k / settlementFeeBpsTimes1k; basis-points × 1000). The per-order builder fee only applies if you opt a builder in with approveBuilder — as an MM placing your own orders you simply pass (address(0), 0) and never pay it.

Escrow: BUYs lock collateral, SELLs lock tokens, refunds recycle via the vault

Placement escrows worst-case value up front: BUYs lock collateral (vault-balance-first, then wallet transferFrom — one approval per pool), SELLs lock the ERC-6909 outcome tokens themselves (one setOperator(pool, true) on the singleton covers every market). Two flows matter for float accounting:

  • Taker surplus and cancel/reduce refunds credit your pool vault balance, not your wallet — and since placement is vault-first, a quoting loop recycles them automatically. withdraw only when rebalancing across pools.
  • Fill proceeds deliver wallet-first with vault fallback. Alert on PayoutFallbackToVault or your reconciliation will drift.

Expiry is mandatory — make it your dead man's switch

Every order carries expireTimestampNs (future unix-ns; there is no "no expiry" sentinel). Set it just past your requote interval rather than far-future: a crashed or partitioned quoter's ladder then ages off the book on its own. Note expired orders are unfillable immediately, but the OrderExpired event is lazy (fires on later cleanup, not at expiry) — prune your own book model on the timestamp, not the event.

Self-match: pick your side, and handle OrderCancelledSelfMatch

With SelfMatchingOption.CancelMaker, the matcher erases your same-owner resting maker and emits OrderCancelledSelfMatch(orderId) — an event-materialized book (yours, and every other participant's) must treat it as a removal or it carries a phantom order afterward. With CancelTaker the incoming order is rejected instead (success == false, no revert). Details in the sharp edges.

Rejections return (false, 0) — they don't revert

A PostOnly that would cross, a FillOrKill that can't fill fully, and an IOC that fills nothing all return success == false from a successful transaction. A loop that only handles reverts will silently under-quote. Conversely cancelOrder / reduceOrder do revert on already-terminal orders — check state before cancelling or you're burning a tx slot in your re-center path.

Markets die on schedule — and respawn

Every market has a hard expiry; venue rolling series (e.g. hourly up/down) spawn a successor each interval. Placement reverts TradingNotActive the moment the market leaves Trading; cancels keep working through Locked. Anything resting at lock is free option value to whoever knows the outcome first — pull quotes ahead of expiry, sweep after resolution (redeem winners — note a resolved market's winning payout is net of the venue's frozen settlement fee, if any — mergeCompleteSet balanced pairs), and watch BinaryMarketsModule's MarketCreated to be resting in the successor the moment it opens.

Infrastructure and latency

The public Somnia RPC nodes — https://api.infra.mainnet.somnia.network/ (mainnet) and https://api.infra.testnet.somnia.network/ (testnet) — are hosted in GCP europe-west4 (Netherlands) and have no rate limit: host your quoting infrastructure in or near that region and submit transactions / read state as fast as your strategy requires. There is no separate market-data service to rate-limit you on this venue — market data is the chain, materialized from the event stream (Raw Integration). A 24/7 operation should keep backup RPCs configured (the authoritative provider list, including WSS endpoints, is at Somnia network-info), fail over on repeated errors, and backfill any missed log range before trusting its book model again.

No session keys — atomicity comes from owning a contract

BinaryPool has no per-user operator approvals (the *For entrypoints are admin-allowlist only), so the operator/fund key split you may run elsewhere doesn't apply. The equivalent isolation and atomic multi-order operations come from making your own contract the order owner: it places a full ladder or cancel-and-replaces both sides in one all-or-nothing transaction, and the book never sees a half-updated ladder.