# Market making on Somnia Markets

> What is different about quoting this venue: batching, operator keys, materializing the book from events, and the edge cases.

What is different about quoting this venue. Somnia Markets runs **spot**
order books, **binary** information markets, and **perps**,
all on the same on-chain order-book engine; this guide covers quoting the
**binary 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
binary 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](./contracts); the SDK
(`@somnia-chain/markets-sdk`) wraps all of it if you're in a JS runtime.
If you quote spot or perps, read the
[batch writes](./contracts#batch-writes) and
[operator keys](./contracts#operator-keys) sections there. Binary
pools reject batch placement and amend with `UseBinaryPlacement`, and they
have no per-user operator keys.

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

Each binary market runs a **single** order book crossing all four order kinds.
Place orders with `placeBinaryOrder` and name the kind explicitly; the generic
`placeOrder` reverts `UseBinaryPlacement`. `userData` is a free tag that the
pool does not read, so you can use it for quote generations. 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. Because of the inversion, a NO buy rests on the ask side:

| `OrderKind` | Value | Book side |
|---|---|---|
| `BUY_YES` | `0` | bid |
| `SELL_YES` | `1` | ask |
| `BUY_NO` | `2` | ask |
| `SELL_NO` | `3` | bid |

## 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 outcome-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: the placement reverts
`SelfMatchCancelTaker`. Details in
[the sharp edges](./contracts#the-sharp-edges).

The protocol pulls your makers on other paths too. A pre-fill guard emits
`OrderCancelledPreFill(orderId)` for every maker it removes before a fill. On a
perp pool a reason tag rides alongside it — one of
`MakerOrderCancelledExceedsPosition`, `MakerOrderCancelledNegativeEquity` or
`MakerOrderCancelledStaleMark`. Treat all four as removals. Handling the base
`OrderCancelledPreFill` alone covers every guard, present and future.

## Rejections revert

A PostOnly that would cross reverts `PostOnlyWouldCross`. A FillOrKill that
cannot fill in full reverts `FillOrKillNotFillable`. An IOC that fills nothing
reverts `ImmediateOrCancelNoFill`. Binary pools have no batch placement, so
every rejection reaches your loop as a revert. Decode the revert data to
choose a recovery. `cancelOrder` / `reduceOrder` also revert on
already-terminal orders. Check state before cancelling, or you burn 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](./contracts#materialize-the-order-book-from-events)).
A 24/7 operation should keep backup RPCs configured (the authoritative
provider list, including WSS endpoints, is at
[Somnia network-info](https://docs.somnia.network/developer/network-info)),
fail over on repeated errors, and backfill any missed log range before
trusting its book model again. The public RPCs accept at most 1,001 blocks
per `eth_getLogs` request. The Hideki testnet RPC is a separate deployment
from the two above; its endpoints are in
[Networks](./contracts#networks).

## 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.

---

Every page is also Markdown: append `.md` to its URL, or ask for `text/markdown`. Found something wrong, missing or confusing in these docs? Tell us: `POST /api/feedback` on this site with JSON `{"message": "…", "context": "what you were doing and what happened", "page": "…"}`, or a plain-text body. `GET /api/feedback` gives the JSON Schema. The team reads every one.
