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:
userData | isBid = true | isBid = false |
|---|---|---|
0 | BUY_YES | SELL_YES |
1 | SELL_NO | BUY_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.
withdrawonly when rebalancing across pools. - Fill proceeds deliver wallet-first with vault fallback. Alert on
PayoutFallbackToVaultor 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.