Somnia Markets
Raw smart-contract integration
Integrate Somnia Markets straight against the contracts, from any language: pools, orders, events, settlement and the sharp edges.
Somnia Markets runs three market families on one on-chain order-book engine:
spot pairs (SpotPool), binary markets (BinaryPool), and
perps (PerpPool). All three extend the same shared
OrderBook core, so the placement surface, the order-lifecycle events, and
the book-materialization technique are identical across every book; what
differs per family is discovery, funding/escrow, and settlement.
This guide is how to trade with nothing but an RPC node and the contract ABIs — any language, any stack. It covers the shared core first, then each family in turn, then materializing a live local order book from the event stream. Pair it with Market Making if you are building a quoting system. (Building a JS/TypeScript app instead? The SDK wraps everything here.)
What you need to start: contract addresses and signatures. Protocol contracts
(the BinaryMarketsModule market registry, the MarketsCore control plane,
the OutcomeToken6909 singleton) are listed live per network on this
explorer's System page; spot pairs and live binary markets are
listed on the markets page; per-market binary addresses are read from
the module's registry on-chain (below). The
complete Solidity interfaces —
everything needed to generate ABIs — are in the
interface reference at the end of this guide.
The RPC and indexer endpoints of each network, and the Hideki testnet
deployment, are in Networks.
Networks
Somnia Markets runs on three public networks. Each RPC serves HTTP and WebSocket requests, and each network has its own indexer.
| Network | Chain id | HTTP RPC | WebSocket RPC | Indexer GraphQL |
|---|---|---|---|---|
| Somnia mainnet | 5031 |
https://api.infra.mainnet.somnia.network |
wss://api.infra.mainnet.somnia.network/ws |
https://prd.smk.somnia.host/v1/graphql |
| Shannon testnet | 50312 |
https://api.infra.testnet.somnia.network |
wss://api.infra.testnet.somnia.network/ws |
https://dev.smk.somnia.host/v1/graphql |
| Hideki testnet | 50383 |
https://api.hideki2.infra.testnet.somnia.network |
wss://api.hideki2.infra.testnet.somnia.network/ws |
https://hideki-dev.smk.somnia.host/v1/graphql |
These RPCs reject an eth_getLogs request whose toBlock is more than 1,000
blocks after its fromBlock (block range exceeds 1000). Page a log
backfill in windows of at most 1,001 blocks.
Hideki testnet
Hideki runs spot, perp and binary markets. The chain was rebuilt on 2026-09-17 with the same chain id. An address from an older Hideki record belongs to the retired chain: on the current chain it can be empty or hold an unrelated contract. Use the addresses below.
| Contract | Address |
|---|---|
PerpPoolFactory |
0xE631fAEE547baeE07353E290f0562ea67d856560 |
MarginBank |
0x478769E1177C0773c43A593DCCc4Ad15F15F1394 |
OperatorPermissionsRegistry |
0x6B2FbfeD328FF5F20C0BE7D3C3437613aFFE8D3a |
SpotPoolRegistry |
0xaC1d9439a6777Bf80D4302Aa08dF1bFc3334Ce9A |
BinaryMarketsModule |
0x3ecC694Cef705358864a646142ac17A90E29e388 |
MarketsCore |
0x2802504314685D89bF6C992CA5a8e7cC78bc0294 |
OutcomeToken6909 |
0xB52c5934113Af5c0Bb20eb3C72290C8215f755b9 |
| USDso (18 decimals), the perp collateral and the spot quote token | 0xed7993A74dAe1e6656B704A424DcD8711aAf4F72 |
| tUSDC (6 decimals), the binary collateral | 0x9E9492cC533E8696A3b6afC7df730800DF75148f |
Perp markets. Each one settles in USDso through the MarginBank above.
PerpPoolFactory.getPerpPools() returns the same list.
| Base | PerpPool |
|---|---|
| BTC | 0x4181dd08E73BbCdd1Ba3E615A18f6abD941b94C0 |
| ETH | 0xD6Cb1DD58580B84D76068aC21594219ce03aeddf |
| SOL | 0xB483ec1fb34996638677c20ab087b16F1896FA53 |
| HYPE | 0x63B47eD4A7DE0FdE659b0d82441dD74828b7297A |
| XRP | 0x7058579F3248559963f79AdB937c6De9f953E060 |
| DOGE | 0xC410012e39765Cd5f772e84BAcB19554AEC040Df |
| BNB | 0x3D2A99fE1e2BdEaA25024515A2D2202DAF9531aD |
| ADA | 0x34fa62909f77699a7a6278D992F72Ff31c7a619D |
| SUI | 0x1Be21287cfB865a7c462Ff3c4f275E70BcccefE4 |
| LINK | 0xf71b619450baFe953F47b9b556416A3A67b443b5 |
| AVAX | 0xde9762B6b441133D1314C68864ae4e63A6eE45C7 |
| XLM | 0x74b05B9B8c8F180b53b7891018F3B254B3E658C9 |
| NEAR | 0x1cD15ae8bFf0FE0a7F49233BE8b90734775bA454 |
| WLD | 0x0F464d57c1FD839E4d87A408BBcCDAb8f77E6394 |
| TAO | 0xD9b6A347A3Ae9FD6BA21179D3343F2c8A922F697 |
Spot pairs. Each one quotes in USDso and is registered in the
SpotPoolRegistry.
| Pair | SpotPool |
Base token |
|---|---|---|
| STT / USDso | 0xbE6c45bF96E1B4e5Ec4655BdaE0F23C0410DB35d |
native STT, reported as 0x28f34DeFd2b4CB48d9eE6d89f2Be4Bc601694c00 |
| WBTC / USDso | 0xD61bb202984E4c02DD6dbF51489A7ec6144e7AA0 |
0xE97673c634EF069Ecc0d95425Cc8e62F99D27E0E (8 decimals) |
| WETH / USDso | 0xFd87D984Bcdd276C3C411C2590acB6a7fF8aB4fF |
0x56115cB05652Ac6117Caa1152B630986a51d36b5 (18 decimals) |
| HBTT / USDso | 0x33a6ec4A0350dcDc6d91C7E475443779bC3f81A9 |
0x0528dD3beEf917DB06A5249C9Ea84b9D702B3A84 (18 decimals) |
Tick, lot and minimum quantity differ by market. Read them from each pool
(getOrderBookParameters() on a perp pool, getPoolParams() on a spot
pool), and read the margin settings of a perp market with
getPerpPoolParameters(). On 2026-10-02 every market above charged zero maker
and taker fees.
On 2026-10-02, isPriceable() on the Hideki perp markets switched between
true and false from one call to the next, a few seconds apart. At a block
where it is false, an order that increases a position reverts
AllSourcesUnavailable (0xc95fe237). The same order succeeds at a block
where it is true. Check isPriceable() before each quote.
Funding a Hideki account. USDso on Hideki is the bridged form of Shannon
USDso, and it cannot be minted directly (mint reverts). Send it from
Shannon over the Hyperlane bridge, to any recipient address. Bridge STT for
gas the same way.
| Token | Shannon router | Hideki router | Sending from Shannon |
|---|---|---|---|
| USDso | 0x94E2E7f4B2B0a8E3032c7D3ff3c86790BB5979E6 |
0xed7993A74dAe1e6656B704A424DcD8711aAf4F72 (the token) |
Approve the Shannon router for Shannon USDso (0x9c32F3827A1a99f0cf9B213de8b53eC3d57bb171), then call transferRemote with no value. |
| STT | 0x67CD0778A719099DAF2D346aD0Db0d9E6FF6674C |
0xB04373932bc05347757da6b8a226d7657584aB35 |
Call transferRemote with msg.value equal to the amount. |
Call transferRemote(uint32 destination, bytes32 recipient, uint256 amount)
with destination 50383 and the recipient address left-padded to 32 bytes.
quoteGasPayment(50383) returns 0 on both Shannon routers, so the call pays
no bridge fee. Delivery is asynchronous. The
Bridge guide covers delivery times,
the reverse direction, and the WBTC, WETH and HBTT routes. Shannon USDso can be
minted by anyone with mint(address to, uint256 amount). Hideki tUSDC has
faucet(uint256 amount), capped at 10,000 tUSDC per call.
The shared order-book core
Binary books take orders through placeBinaryOrder instead (see
the order kind). Spot and perp books
expose the same placement entrypoint, covering all order types (GTC /
NormalOrder, FillOrKill, ImmediateOrCancel, PostOnly):
function placeOrder(
bool isBid, uint64 userData, uint256 price, uint256 quantity,
uint64 expireTimestampNs, OrderType orderType,
SelfMatchingOption selfMatchingOption,
address builder, uint96 builderFeeBpsTimes1k
) external payable returns (bool success, OrderId id);
expireTimestampNsis mandatory and nanoseconds — a future unix-ns timestamp (now_seconds * 1e9 + lifetime_ns); zero or past values are rejected. There is no "no expiry" sentinel.- A rejected order reverts. A single-order placement does not return
success == false; it reverts with a typed error. APostOnlythat would cross revertsPostOnlyWouldCross. AFillOrKillthat cannot fill in full revertsFillOrKillNotFillable. An IOC that fills nothing revertsImmediateOrCancelNoFill. A self-match underCancelTakerrevertsSelfMatchCancelTaker. An expiry in the past revertsOrderAlreadyExpired. Decode the revert data to choose a recovery. The batch calls handle a rejection differently (see Batch writes). - Quantize in integers.
getOrderBookParameters()returnstickSize,lotSize, andminQuantityin raw token units; off-grid values revertInvalidPrice/InvalidQuantity. builder/builderFeeBpsTimes1kattribute the order to a routing/builder frontend and pay it a per-order fee (basis-points × 1000). Pass(address(0), 0)for none. A non-zero fee requires the order owner to have opted the builder in first —approveBuilder(builder, maxFeeBpsTimes1k)on the pool (0 revokes) — and is capped by the pool'sgetMaxBuilderFeeBpsTimes1k(); charges emitBuilderFeeCharged. Spot, binary and perp pools all accept builder values; an unapproved non-zero fee revertsBuilderNotApproved(spot),BuilderFeeExceedsCap(binary) orBuilderFeeExceedsApproval(perp).cancelOrder(id)/reduceOrder(id, newQty)manage resting orders; both revert (rather than no-op) on already-terminal orders — check your local book orgetOrder(id)first.cancelExpiredOrders(ids)andsweepExpiredAtLevel(isBid, price, maxCount)are permissionless cleanup — the SDK'sclient.listSweepableOrders({ pool?, marketType? })is the work-list for both, returningorderId/isBid/priceper row. Note it is notstatus: "Expired": that status means the order was already swept, whereas a keeper wants orders stillOpenwhose expiry has passed.OrderIds are unique per pool — always key by(pool, orderId).
Reading any book on demand, via eth_call:
| View | Returns |
|---|---|
getBookLevels(isBid, numLevels) |
Aggregated (price, quantity) levels, best first |
getAllOpenOrdersOffChain(isBid, maxCount, startCursor) |
Full Order structs, paginated (msg.sender must be address(0) — the eth_call default) |
getOrder(orderId) / getOwnOpenOrders() |
Single order / caller's open ids |
Polling these is fine for a dashboard; a trading system should take one snapshot and apply event deltas — see materialization.
Batch writes
A quoting loop often changes several orders at once. Spot and perp pools
accept these changes in one transaction. Binary pools reject placeOrders
and amendOrders with UseBinaryPlacement.
placeOrders(requests)places each request in array order and returns(successes, ids), aligned withrequests. A request that the book rejects when it lands (a PostOnly that would cross, a FillOrKill or IOC that cannot fill, a self-match underCancelTaker, or an expiry in the past) does not revert the call: its flag isfalse, its id is0, and the other requests still apply. Any other failure reverts the whole batch, for example an off-grid price (InvalidPrice) or a request that the owner cannot fund.cancelOrders(orderIds)returns one flag per id. It returnsfalsefor an order that is no longer live, wherecancelOrderreverts.reduceOrders(requests)applies one(orderId, newQuantityRemaining)reduction per request.amendOrder(request)re-quotes the resting orderoldOrderIdwith the terms innewOrder, in one transaction. Use the id that it returns: it can be different fromoldOrderId. A failed amendment reverts with an error that names the reason, for examplePostOnlyWouldCrossorAmendOldOrderGone.amendOrders(requests)amends several orders in one transaction. Deployed pools do not all report a failed request in the same way. Some pools revert the whole batch:AmendReplacementRejected(requestIndex, reason)for a rejected replacement, orAmendOldOrderGonefor an old order that is no longer live. Other pools return id0for that request and apply the other requests. Handle both results: catch the revert, and check every returned id. On every pool, a failure that is not a rejection, for example an off-grid price, reverts the whole batch.- An empty array reverts
EmptyBatch. - The batch calls are not
payable. A native-token amount that a batch order needs must already be in your vault balance on that pool (depositNative).
Operator keys
A market maker often holds funds in one key and signs orders with a second key. Spot and perp pools support this split through an operator registry. Binary pools do not (see Market Making).
- Read the registry address from the pool with
getOperatorPermissionsRegistry(). - From the owner key, give the operator key the function selectors that it
can call.
setOperatorApprovalForPool(pool, operator, selectors, true)covers one pool.setOperatorApprovalGlobal(operator, selectors, true)covers every official pool.setOperatorDenialForPoolremoves one pool from a global grant. - From the operator key, call the
…Forentry points with the owner's address:placeOrderFor(owner, …),cancelOrderFor(owner, orderId)andreduceOrderFor(owner, orderId, newQuantityRemaining). The selector to grant is the selector of that function:0x80054449forplaceOrderFor,0xe37b444bforcancelOrderFor,0x364c2587forreduceOrderFor.
The batch and amend operator entry points use the same grants.
placeOrdersFor needs the placeOrderFor grant, cancelOrdersFor needs the
cancelOrderFor grant, and reduceOrdersFor needs the reduceOrderFor
grant. amendOrderFor and amendOrdersFor need both the placeOrderFor
and the cancelOrderFor grants.
The order belongs to the owner, so its funds and its proceeds are the
owner's. A call without a grant reverts OnlyApprovedContracts. Read a grant
from the pool with isOperatorAuthorized(owner, operator, selector).
Two rules need care:
- A global grant is broad. On the testnets it also covers every perp pool, so
a global
placeOrderForgrant lets the operator open leveraged positions for the owner. Grant per pool to keep an operator on one market. - On a perp pool, an order that the operator sends does not top up margin from the owner's wallet. Keep the owner's MarginBank balance funded (see Perps).
Spot markets
Each trading pair is its own SpotPool — live pairs are listed on the
markets page. getPoolParams() returns everything static in one call:
(baseToken, quoteToken, makerFeeBpsTimes1k, takerFeeBpsTimes1k, tickSize, minQuantity, lotSize). A native-token side is reported as a sentinel token
address (not address(0)) — resolve it from getPoolParams rather than
assuming.
Spot semantics are the conventional ones: isBid = true buys base with
quote, a fill is a base ↔ quote swap at the maker's resting price, and
userData is a free 64-bit tag echoed back on the order struct and its
OrderPlaced event. Use it for strategy/generation ids.
Funding. By default the pool auto-pulls the required input from your
wallet at placement (ERC-20 transferFrom after a one-time approval, or
msg.value on native pools) and auto-delivers proceeds back on fill,
cancel, or expiry. Size the pull with getAutoPullRequirement(owner, isBid, price, quantity, builderFeeBpsTimes1k). High-churn integrations should opt
out with setManualVaultMode(true): deposit once, quote against the vault
balance with no per-order token transfers, withdraw when rebalancing —
for a loop that places and cancels constantly the gas savings compound.
Mark price. Spot pools push MarkPriceUpdated(asset, markPrice, rawMidpoint) whenever the book midpoint advances — an EMA-smoothed midpoint
plus the raw (bestBid + bestAsk) / 2, an on-chain fair-price feed with no
extra infrastructure.
Binary markets
Discover markets
BinaryMarketsModule is the market registry (the MarketsCore control plane
holds only operators, venues, and module bindings — no markets). Each market
is a MarketRecord (read via module.markets(marketId)):
| Field | Use |
|---|---|
pool |
The BinaryPool — the order book you trade on |
market |
The BinaryMarket — lifecycle + resolution state |
collateral |
The ERC-20 quoted against (one per market) |
originOperatorId / originVenueId |
Origin attribution: the uint32 operator id + opaque bytes32 venue id the market was created under |
yesId / noId |
ERC-6909 ids of the outcome positions on the shared OutcomeToken6909 singleton |
tradingStart / expiry |
Trading window (unix seconds) |
Watch the module's MarketCreated event to discover new markets — every
market fires it, whether created directly via createMarket or rolled by a
venue's MarketCreator (venues run rolling series — hourly up/down and
similar — that create markets continuously), and it is the only creation
event carrying the (operatorId, venueId) origin. marketId is a
module-scoped counter (bytes32(++marketSeq)) that doubles as the CREATE2
salt — unique by construction, not precomputable before the create tx.
module.marketIdByAddress(addr) is the reverse lookup.
Every market carries an immutable kind tag, IMarket.marketType(). This
section describes BINARY_SINGLE_BOOK — the only kind live today. Future
kinds (dual-book, multi-outcome) will carry different tags and different book
semantics: branch on the tag, never on assumptions.
Lifecycle: Listed → Trading → Locked → (Settling) → Resolved | Voided
(BinaryMarket.StatusChanged, plus Resolved / Voided). Orders are only
accepted while the market reports Trading — placement reverts
TradingNotActive otherwise. Cancellation stays open through Locked;
redeem opens at Resolved / Voided.
One book, two outcomes: the order kind
Each BinaryPool runs a single order book. Place an order with
placeBinaryOrder and name its kind:
OrderKind |
Value | Book side | Locks at placement |
|---|---|---|---|
BUY_YES |
0 |
bid | collateral |
SELL_YES |
1 |
ask | YES tokens |
BUY_NO |
2 |
ask | collateral |
SELL_NO |
3 |
bid | NO tokens |
A binary pool rejects placeOrder, placeOrders, amendOrder and
amendOrders with UseBinaryPlacement. userData is
the last argument of placeBinaryOrder and is a free 64-bit tag, as on spot:
the pool stores it and does not read it. placeBinaryOrder takes no native
value; a non-zero msg.value reverts UnexpectedNativeDeposit.
The NO book is price-inverted so the base matcher can cross the two books
without knowing about outcomes: a NO order is submitted at the YES-side
complement, price = oneCollateral − noPrice, where
oneCollateral = 10 ** collateral.decimals(). All prices must satisfy
0 < price < oneCollateral, because a share can never be worth more than one
collateral unit. A price at or above oneCollateral reverts
PriceOutOfBounds, and a zero price reverts InvalidPrice.
Escrow and funding
Placement escrows worst-case value up front:
- BUY orders (
BUY_YES,BUY_NO) lock collateral (ceil-roundedprice × quantityin the order's own frame). The pull is vault-first: free vault balance is consumed beforetransferFromon your wallet — socollateral.approve(pool, …)once per pool. (There is no auto-pull/manual-vault toggle here; this is the only mode.) - SELL orders (
SELL_YES,SELL_NO) lock the outcome tokens themselves, pulled from your ERC-6909 balance on the singleton — calloutcomeToken.setOperator(pool, true)once; it covers every market, since ids are namespaced by pool.
Money flows back through two channels worth knowing:
- Refunds land in your vault balance — taker surplus (you locked at your
limit price, filled at a better maker price) and cancel/reduce/expiry
refunds credit the pool vault, not your wallet. Because placement is
vault-first, a quoting loop recycles these automatically;
withdraw(collateral, amount)only when rebalancing. Read it withgetWithdrawableBalance(owner, collateral). - Fill proceeds deliver wallet-first with vault fallback — if the wallet
transfer fails the pool credits the vault and emits
PayoutFallbackToVault.
Inventory comes from complete sets:
module.mintCompleteSet(operatorId, venueId, marketId, amount) turns amount
collateral into amount YES + amount NO (setOperator(module, true) once
for the merge/redeem directions; operatorId/venueId are routing
attribution only — pass 0 / bytes32(0) for none);
mergeCompleteSet(operatorId, venueId, marketId, amount) is the inverse;
redeem(operatorId, venueId, marketId, outcomeIdx, amount) burns the winning
side 1:1 after resolution (or either side at 1/2 on void). You can also call
the pool's mintSet / burnSet / redeem directly. Note you often need
no inventory at all to quote — see the fill paths below.
On a resolved market (never a voided one), the pool skims the venue's
frozen settlementFeeBpsTimes1k ONCE from the whole winning backing when the
first winning redeem lands (emitting SettlementFeeCharged(feeRecipient, winningBacking, fee)); every winner then redeems for 1 − fee per token.
Per-fill maker/taker fees (ProtocolFeeCharged) and the per-order builder
fee (BuilderFeeCharged) are the other two fee rails — all rates are frozen
into the pool at creation from the origin venue's config and readable via
getBinaryPoolParams().
The four fill paths
Two opposite-kind orders crossing settle by one of four paths (dispatched on the unordered pair of order kinds):
| Crossing pair | Path | Settlement |
|---|---|---|
BUY_YES × SELL_YES |
DIRECT_YES | YES ↔ collateral swap |
SELL_NO × BUY_NO |
DIRECT_NO | NO ↔ collateral swap |
BUY_YES × BUY_NO |
MINT_A_PAIR | Both pay collateral; pool mints a fresh pair, one side each (emits SetMinted) |
SELL_YES × SELL_NO |
BURN_A_PAIR | Both escrows burned as a pair; each seller paid their share (emits SetBurned) |
The fill price on OrderFilled is always the maker's resting price, in
the YES frame; the NO-side display price is oneCollateral − fillPrice.
There is no on-chain "fill kind" field — consumers derive it from the two
orders' kinds — the SDK exports
sideOfKind (the YES/NO
side of a resting order, from the pool's BinaryOrderPlaced(orderId, kind)
event) and fillKind for
exactly this derivation.
getOrderInfo(orderId) exposes the binary-side escrow state per order
(lockedCollateral / lockedOutcome, kind).
Perps
PerpPool extends the same OrderBook core, so everything in
the shared core and
materialization carries over
unchanged. On top it adds margin accounting, funding, and liquidations —
with FundingUpdated / OpenInterestUpdated events, and a dedicated
MakerOrderCancelledExceedsPosition removal event (a maker order erased
pre-fill because it would breach the owner's position limit — treat it as a
removal in a book reducer). Perp markets are live on the Shannon and Hideki
testnets (see Perps).
Batch writes and operator keys work on perp
pools as on spot pools, with the differences listed in those sections.
Discovery. PerpPoolFactory.getPerpPools() lists the markets of a
network. getBaseTokenForPool(pool) returns the base token of one, and
getPoolForBaseToken(baseToken) is the reverse lookup. The factory is
0x516d2cb6de41bE98444e887793946E65989Ba07b on Shannon; the Hideki factory
and markets are in Hideki testnet.
getMarketStatus() returns 0 (Active), 1 (ReduceOnly), 2
(Settlement) or 3 (Delisted). Only an Active market accepts an order
that increases a position. isPriceable() is false while the market cannot
read a usable mark price, so check it before quoting. Read the mark price
with getMarkPrice() or the non-reverting tryGetMarkPrice(), and the oracle
index price and its update time with getIndexPrice().
getMaxRestingOrdersPerAccount() is the most orders that one account can
rest on the market (0 means the pool applies its built-in default).
Prices and sizes. A price is in collateral units (USDso, 18 decimals) per
one whole base token. getOneBase() returns one whole base token in raw
units, so the notional of an order is price × quantity / getOneBase().
Margin. A perp pool keeps collateral in its marginBank(). Approve the
MarginBank to spend the collateral token. deposit(amount) and
withdraw(amount) move margin, and getAccountEquity(account) reads an
account's equity in the bank. An order that its owner places also tops up
the margin it needs from the owner's wallet through the same allowance, so a
deposit is optional. Without the allowance, and with no margin in the bank,
the placement reverts in the token's transferFrom. An order that an operator
places does not top up (see Operator keys).
Materialize the order book from events
Every book mutation emits an event, so one pinned snapshot plus the log stream reproduces the full book — for any of the three families. This is exactly how the protocol's own indexer and the SDK's live watches work — the reducer below is the same one they run (see Architecture for the SDK's implementation of it).
Snapshot, then stream
- Pin a block
N(eth_blockNumber). - Snapshot both sides with
getAllOpenOrdersOffChainat blockN(pass the block tag explicitly so pagination pages are mutually consistent). Drop orders whoseexpireTimestampNshas already passed. - Stream the pool's logs from
N + 1(eth_subscribe("logs")over WSS, or aneth_getLogsloop) and apply them strictly in(blockNumber, logIndex)order.
The event reducer
Keep a map orderId → Order plus per-side level aggregates:
| Event | Action |
|---|---|
OrderPlaced(orderId, placedOrder) |
Cache the struct; do not insert. Fires for every accepted order — placedOrder.quantityRemaining is already the post-match residual (the matcher runs before the event), so zero remaining means it fully filled on entry. |
OrderRested(orderId) |
Insert the cached order into the book. This is the only insert signal. |
OrderFilled(takerId, makerId, qty, takerRem, makerRem, fillPrice) |
Patch the maker: remaining = makerRem, remove at zero. Ignore the taker leg — its state arrives on its own OrderPlaced/OrderRested later in the same tx. |
OrderReduced(orderId, newQuantity) |
Set remaining = newQuantity (not a fill). |
OrderCancelled(orderId) / OrderExpired(orderId) |
Remove (idempotently). |
OrderCancelledSelfMatch(orderId) |
Remove — a same-owner resting maker erased by a CancelMaker self-match. |
OrderCancelledPreFill(orderId) |
Remove — a resting maker pulled by a pre-fill guard before it could fill. The matching loop emits it for every pre-fill removal, so handling it covers every present and future guard. |
Family extras, none of which mutate book state: spot pools also emit
MarkPriceUpdated (fair-price telemetry); binary pools emit SetMinted /
SetBurned / Redeemed / SettlementFeeCharged (outcome-supply /
settlement telemetry — mint-a-pair and burn-a-pair fills emit the first two
alongside OrderFilled) and BinaryMarket.StatusChanged (leaves Trading →
no new fills, only cancels from here); perp pools add
MakerOrderCancelledExceedsPosition, MakerOrderCancelledNegativeEquity and
MakerOrderCancelledStaleMark, which do mutate the book — treat all three
as removals. They are reason tags emitted alongside the base
OrderCancelledPreFill, so a reducer that handles the base event alone already
removes the order.
Ordering inside a placement tx: settlement hooks and one OrderFilled per
matched maker fire first, then the taker's OrderPlaced (already
residual-adjusted), then OrderRested iff the residual rested. A reducer that
only inserts on OrderRested handles this for free.
The sharp edges
- Self-match removals have their own event. With
SelfMatchingOption.CancelMaker, the matcher erases the same-owner resting maker and emitsOrderCancelledSelfMatch(orderId)(on every book — spot, binary, perp). Handle it as a removal or your reducer carries a phantom order whenever any trader self-crosses withCancelMaker. WithCancelTakerthe incoming order is rejected instead: a single-order placement revertsSelfMatchCancelTaker, andplaceOrdersreturnsfalsefor that request. - Expiry is lazy. Matching walks past expired makers without cleanup;
OrderExpiredonly fires later (owner's next placement, or the permissionless sweeps). Prune locally the momentexpireTimestampNspasses — an expired order can never fill — and treat the eventual event as a no-op. - Make removals idempotent. Between local pruning and the several removal paths, "remove an already-absent order" must be a no-op.
- Stream gaps. On WSS reconnect, backfill the missed range with
eth_getLogsbefore resuming, in windows of at most 1,001 blocks (see Networks). Somnia blocks have instant BFT finality, so a delivered log is final and no reorg handling is needed; the SDK's own live tail does none. If you still want a guard, treat a parent-hash discontinuity as a reason to re-snapshot rather than to unwind.
Track your own orders and fills
Both order-id parameters on OrderFilled are indexed topics, so you can filter
your fills by the ids your placeOrder or placeBinaryOrder calls returned (or
getOwnOpenOrders() after a restart). Alert on PayoutFallbackToVault — a
delivery to your wallet failed and the proceeds are parked in your vault
balance. Reconcile funds with getWithdrawableBalance(owner, token) and, on
binary markets, outcomeToken.balanceOf(owner, yesId | noId) for positions.
Interface reference
ABI-faithful excerpts of every function and event used in this guide, taken
from the deployed contracts. Admin/venue-governance entrypoints are omitted;
external interface types (IERC20, adapters) are shown as address — the ABI
encoding is identical.
Shared order-book core (every pool)
type OrderId is uint128;
enum OrderType { NormalOrder, FillOrKill, ImmediateOrCancel, PostOnly }
enum SelfMatchingOption { CancelTaker, CancelMaker }
struct Order {
OrderId orderId;
bool isBid;
address owner;
uint64 userData;
uint256 price;
uint256 fullQuantity;
uint256 quantityRemaining;
uint64 expireTimestampNs;
}
struct OrderBookLevel {
uint256 price;
uint256 quantity;
}
struct OrderBookParameters {
uint256 tickSize; // min price increment, raw quote/collateral units
uint256 minQuantity; // min order quantity, raw base units
uint256 lotSize; // min quantity increment, raw base units
}
interface IOrderBook {
event OrderPlaced(OrderId indexed orderId, Order placedOrder);
event OrderRested(OrderId indexed orderId);
event OrderFilled(
OrderId indexed takerOrderId,
OrderId indexed makerOrderId,
uint256 quantityFilled,
uint256 takerRemainingQuantity,
uint256 makerRemainingQuantity,
uint256 fillPrice
);
event OrderCancelled(OrderId indexed orderId);
event OrderCancelledSelfMatch(OrderId indexed orderId);
event OrderExpired(OrderId indexed orderId);
event OrderReduced(OrderId indexed orderId, uint256 newQuantity);
event OrderBookParametersUpdated(OrderBookParameters newParameters);
function placeOrder(
bool isBid,
uint64 userData,
uint256 price,
uint256 quantity,
uint64 expireTimestampNs,
OrderType orderType,
SelfMatchingOption selfMatchingOption,
address builder,
uint96 builderFeeBpsTimes1k
) external payable returns (bool success, OrderId id);
function cancelOrder(OrderId orderId) external;
function reduceOrder(OrderId orderId, uint256 newQuantityRemaining) external;
function cancelExpiredOrders(OrderId[] calldata orderIds) external;
function sweepExpiredAtLevel(bool isBid, uint256 price, uint256 maxCount)
external returns (uint256 cleaned);
function getOrder(OrderId orderId) external view returns (Order memory);
function getOwnOpenOrders() external view returns (OrderId[] memory);
function getBookLevels(bool isBid, uint64 numLevels)
external view returns (OrderBookLevel[] memory);
function getOrderBookParameters() external view returns (OrderBookParameters memory);
// eth_call only — msg.sender must be address(0)
function getAllOpenOrdersOffChain(bool isBid, uint256 maxCount, uint64 startCursor)
external view returns (Order[] memory orders, bool hasMoreOrders, uint64 nextCursor);
}
// The per-user vault, inherited by every pool.
interface IVault {
function deposit(address token, uint256 amount) external;
function depositNative() external payable;
function withdraw(address token, uint256 amount) external;
function getWithdrawableBalance(address owner, address token)
external view returns (uint256);
}
// Rejections of a single-order placement (every pool).
error OrderAlreadyExpired();
error SelfMatchCancelTaker();
error PostOnlyWouldCross();
error FillOrKillNotFillable();
error ImmediateOrCancelNoFill();
Batch writes and operator entry points (spot and perp pools)
struct PlaceOrderRequest {
bool isBid;
uint64 userData;
uint256 price;
uint256 quantity;
uint64 expireTimestampNs;
OrderType orderType;
SelfMatchingOption selfMatchingOption;
address builder;
uint96 builderFeeBpsTimes1k;
}
struct ReduceOrderRequest {
OrderId orderId;
uint256 newQuantityRemaining;
}
struct AmendOrderRequest {
OrderId oldOrderId;
bool alwaysPlace;
PlaceOrderRequest newOrder;
}
// IOrderBook, continued.
interface IOrderBook {
// Batch writes and amend. None of them is payable.
function placeOrders(PlaceOrderRequest[] calldata requests)
external returns (bool[] memory successes, OrderId[] memory ids);
function cancelOrders(OrderId[] calldata orderIds)
external returns (bool[] memory cancelled);
function reduceOrders(ReduceOrderRequest[] calldata requests) external;
function amendOrder(AmendOrderRequest calldata request)
external returns (OrderId newOrderId);
function amendOrders(AmendOrderRequest[] calldata requests)
external returns (OrderId[] memory newOrderIds);
// Called by an operator key for `owner`.
function placeOrderFor(
address owner,
bool isBid,
uint64 userData,
uint256 price,
uint256 quantity,
uint64 expireTimestampNs,
OrderType orderType,
SelfMatchingOption selfMatchingOption,
address builder,
uint96 builderFeeBpsTimes1k
) external payable returns (bool success, OrderId id);
function cancelOrderFor(address owner, OrderId orderId) external;
function reduceOrderFor(address owner, OrderId orderId, uint256 newQuantityRemaining) external;
function placeOrdersFor(address owner, PlaceOrderRequest[] calldata requests)
external returns (bool[] memory successes, OrderId[] memory ids);
function cancelOrdersFor(address owner, OrderId[] calldata orderIds)
external returns (bool[] memory cancelled);
function reduceOrdersFor(address owner, ReduceOrderRequest[] calldata requests) external;
function amendOrderFor(address owner, AmendOrderRequest calldata request)
external returns (OrderId newOrderId);
function amendOrdersFor(address owner, AmendOrderRequest[] calldata requests)
external returns (OrderId[] memory newOrderIds);
}
// Declared on both ISpotPool and IPerpPool.
interface ISpotPool {
function getOperatorPermissionsRegistry() external view returns (address);
function isOperatorAuthorized(address owner, address operator, bytes4 selector)
external view returns (bool);
}
interface IOperatorPermissionsRegistry {
function setOperatorApprovalForPool(address pool, address operator, bytes4[] calldata selectors, bool approved)
external;
function setOperatorApprovalGlobal(address operator, bytes4[] calldata selectors, bool approved) external;
function setOperatorDenialForPool(address pool, address operator, bytes4[] calldata selectors, bool denied)
external;
function isApproved(address owner, address operator, bytes4 selector, address pool)
external view returns (bool);
}
error EmptyBatch();
error AmendOldOrderGone(OrderId oldOrderId);
error AmendReplacementRejected(uint256 requestIndex, uint8 reason); // on pools where amendOrders reverts the whole batch
error OnlyApprovedContracts();
error UseBinaryPlacement(); // a binary pool, on placeOrder, placeOrders and amend
SpotPool additions
interface ISpotPool /* is IOrderBook, IVault */ {
event MarkPriceUpdated(address indexed asset, uint256 markPrice, uint256 rawMidpoint);
event ManualVaultModeUpdated(address indexed user, bool enabled);
event PayoutFallbackToVault(address indexed owner, address indexed token, uint256 amount);
function setManualVaultMode(bool enabled) external;
function getPoolParams() external view returns (
address baseToken,
address quoteToken,
uint256 makerFeeBpsTimes1k,
uint256 takerFeeBpsTimes1k,
uint256 tickSize,
uint256 minQuantity,
uint256 lotSize
);
function getManualVaultMode(address user) external view returns (bool enabled);
function getAutoPullRequirement(
address owner,
bool isBid,
uint256 price,
uint256 quantity,
uint96 builderFeeBpsTimes1k
) external view returns (address inputToken, uint256 requiredAmount, uint256 delta);
function getMidpointEmaState() external view returns (uint256 emaValue, uint64 lastUpdateNs);
}
BinaryPool additions
enum OrderKind { BUY_YES, SELL_YES, BUY_NO, SELL_NO }
struct BinaryPoolOrderInfo {
OrderId orderId;
uint256 lockedCollateral; // BUY escrow (mutually exclusive with lockedOutcome)
uint256 lockedOutcome; // SELL escrow
uint8 outcomeIdx; // 0 = YES, 1 = NO
bool isBuy;
address builder;
uint96 builderFeeBpsTimes1k;
}
struct BinaryPoolInfo {
address collateralToken;
address market;
address outcomeToken;
uint256 yesId;
uint256 noId;
uint256 oneCollateral; // 10 ** collateral.decimals()
uint256 setBacking;
address feeRecipient;
uint256 makerFeeBpsTimes1k;
uint256 takerFeeBpsTimes1k;
uint256 maxBuilderFeeBpsTimes1k;
uint256 settlementFeeBpsTimes1k;
}
interface IBinaryPool /* is IOrderBook, IVault */ {
event BinaryOrderPlaced(OrderId indexed orderId, OrderKind kind);
event SetMinted(address indexed payer, address indexed yesTo, address indexed noTo, uint256 amount);
event SetBurned(address indexed holder, uint256 amount);
event Redeemed(
address indexed holder,
address indexed to,
uint8 outcomeIdx,
uint256 amountBurned,
uint256 collateralOut
);
// Fee rail: per-fill protocol fees, per-order builder fees (opt-in via
// approveBuilder), and the one-time settlement fee on the winning backing.
event ProtocolFeeCharged(
OrderId indexed orderId, address indexed recipient, address indexed token, uint256 amount, bool isTakerSide
);
event BuilderApproved(address indexed user, address indexed builder, uint256 maxFeeBpsTimes1k);
event BuilderFeeCharged(OrderId indexed orderId, address indexed builder, address indexed token, uint256 amount);
event SettlementFeeCharged(address indexed feeRecipient, uint256 winningBacking, uint256 fee);
event PayoutFallbackToVault(address indexed owner, address indexed token, uint256 amount);
// The placement entrypoint for binary pools. `price` is the YES-side price
// for every kind. A non-zero msg.value reverts UnexpectedNativeDeposit.
function placeBinaryOrder(
OrderKind kind,
uint256 price,
uint256 quantity,
uint64 expireTimestampNs,
OrderType orderType,
SelfMatchingOption selfMatchingOption,
address builder,
uint96 builderFeeBpsTimes1k,
uint64 userData
) external payable returns (bool success, OrderId id);
function approveBuilder(address builder, uint256 maxFeeBpsTimes1k) external;
function getMaxBuilderFeeBpsTimes1k() external view returns (uint256 maxFeeBpsTimes1k);
function getBuilderApproval(address user, address builder) external view returns (uint256 maxFeeBpsTimes1k);
function getEffectiveBuilderApproval(address user, address builder) external view returns (uint256 maxFeeBpsTimes1k);
function mintSet(address yesTo, address noTo, uint256 amount) external;
function burnSet(uint256 amount) external;
function redeem(uint256 amount, uint8 outcomeIdx, address to)
external returns (uint256 collateralOut);
function collateralToken() external view returns (address);
function outcomeToken() external view returns (address);
function outcomeId(uint8 outcomeIdx) external view returns (uint256);
function market() external view returns (address);
function setBacking() external view returns (uint256);
function getBinaryPoolParams() external view returns (BinaryPoolInfo memory info);
function getOrderInfo(OrderId orderId) external view returns (BinaryPoolOrderInfo memory info);
}
error UseBinaryPlacement(); // placeOrder, placeOrders or amend on a binary pool
error UnexpectedNativeDeposit(); // msg.value on placeBinaryOrder
error PriceOutOfBounds(); // price at or above oneCollateral
BinaryMarketsModule (the market registry)
enum VoidPolicy { UNIFORM, AMM_SNAPSHOT } // AMM_SNAPSHOT is a dead legacy slot
enum MarketType { BINARY_SINGLE_BOOK, BINARY_DUAL_BOOK, NATIVE_MULTI }
interface IBinaryMarketsModule {
event MarketCreated(
bytes32 indexed marketId,
address indexed market,
address indexed pool,
uint256 oracleQuestionId,
uint32 operatorId,
bytes32 venueId,
address creator,
address collateral,
uint256 yesId,
uint256 noId,
uint8 outcomeSlotCount,
MarketType marketType,
uint64 tradingStart,
uint64 expiry,
VoidPolicy voidPolicy,
string asset,
uint256 strike, // 0 for reference-mode markets (no fixed strike)
string question,
bytes context
);
function markets(bytes32 marketId) external view returns (
uint256 oracleQuestionId,
uint8 outcomeSlotCount,
VoidPolicy voidPolicy,
address collateral,
uint32 originOperatorId,
bytes32 originVenueId,
address oracleAdapter,
address creator,
address market,
address pool,
uint256 yesId,
uint256 noId,
uint64 tradingStart,
uint64 expiry
);
function marketIdByAddress(address market) external view returns (bytes32 marketId);
// operatorId / venueId are routing attribution only (0 / bytes32(0) = none).
function mintCompleteSet(uint32 operatorId, bytes32 venueId, bytes32 marketId, uint256 amount) external;
function mergeCompleteSet(uint32 operatorId, bytes32 venueId, bytes32 marketId, uint256 amount) external;
function redeem(uint32 operatorId, bytes32 venueId, bytes32 marketId, uint8 outcomeIdx, uint256 amount) external;
}
BinaryMarket (lifecycle) and the outcome-token singleton
enum MarketStatus { Listed, Trading, Locked, Settling, Resolved, Voided }
interface IBinaryMarket {
event StatusChanged(MarketStatus indexed oldStatus, MarketStatus indexed newStatus);
event Resolved(uint8 indexed winningOutcome);
event Voided();
function marketType() external view returns (MarketType);
function tradingActive() external view returns (bool);
function isResolved() external view returns (bool);
function isVoided() external view returns (bool);
function winningOutcome() external view returns (uint8);
}
// ERC-6909 subset. One singleton for all markets; id = (uint160(pool) << 8) | outcomeIdx.
interface IOutcomeToken6909 {
event Transfer(
address caller,
address indexed sender,
address indexed receiver,
uint256 indexed id,
uint256 amount
);
function setOperator(address spender, bool approved) external returns (bool);
function transferFrom(address from, address to, uint256 id, uint256 amount)
external returns (bool);
function balanceOf(address owner, uint256 id) external view returns (uint256);
}
Perps (factory, pool additions, MarginBank)
interface IPerpPoolFactory {
function getPerpPools() external view returns (address[] memory);
function getBaseTokenForPool(address perpPool) external view returns (address);
function getPoolForBaseToken(address baseToken) external view returns (address);
}
enum MarketStatus { Active, ReduceOnly, Settlement, Delisted }
struct PerpPoolParameters {
uint256 initialMarginBps;
uint256 maintenanceMarginBps;
uint256 closeOutMarginBps;
uint256 maxOpenInterest;
uint256 maxPositionSize;
uint256 takerFeeBpsTimes1k;
int256 makerFeeBpsTimes1k;
uint256 insuranceFundShareBps;
}
interface IPerpPool /* is IOrderBook */ {
function marginBank() external view returns (address);
function getMarketStatus() external view returns (MarketStatus);
function isPriceable() external view returns (bool);
function getMarkPrice() external view returns (uint256);
function tryGetMarkPrice() external view returns (bool ok, uint256 price);
function getIndexPrice() external view returns (uint256 price, uint256 updatedAt);
function getOneBase() external view returns (uint256);
function getPerpPoolParameters() external view returns (PerpPoolParameters memory);
function getMaxRestingOrdersPerAccount() external view returns (uint16);
}
interface IMarginBank {
function deposit(uint256 amount) external;
function withdraw(uint256 amount) external;
function getAccountEquity(address account) external view returns (int256);
}
Bridge routers and testnet tokens
// Hyperlane token router (the USDso and STT routers in Hideki testnet).
interface ITokenRouter {
function transferRemote(uint32 destination, bytes32 recipient, uint256 amount)
external payable returns (bytes32 messageId);
function quoteGasPayment(uint32 destination) external view returns (uint256);
}
interface IShannonUSDso {
function mint(address to, uint256 amount) external;
}
interface ITestUSDC {
function faucet(uint256 amount) external; // at most 10,000 tUSDC per call
}