The exchange API
The SomniaMarkets class is the SDK's primary surface: markets keyed by symbols,
fetch*/watch*/createOrder verbs, human-unit structs — the idioms every
exchange bot already speaks (if you've driven a venue through ccxt, nothing
here will surprise you, down to the field names). Under the hood it runs on
the native engine (scoped watches, local books, one-round-trip writes), so the
watch* channels are zero-round-trip and createOrder confirms in a single
round-trip.
import { SomniaMarkets } from "@somnia-chain/markets-sdk";
const exchange = new SomniaMarkets({ indexerUrl, chain, wsRpcUrl, privateKey }); // one per chain
await exchange.loadMarkets();
const symbol = "BTC-95000-31DEC26/USDC#YES";
const book = await exchange.watchOrderBook(symbol, 10); // live, zero RTT
const order = await exchange.createOrder(symbol, "limit", "buy", 10, 0.62);
// … later:
await exchange.cancelOrder(order.id, symbol);
The raw engine stays available at exchange.client — bigint-exact,
address-keyed — for anything the exchange verbs don't cover; see
the engine guide.
Symbols
A symbol names a market; a tradable symbol names something you place orders on. For spot they coincide; for outcome markets each outcome is its own tradable:
| Kind | Market symbol | Tradables |
|---|---|---|
| Spot | SOMI/USDC | SOMI/USDC |
| Binary | BTC-95000-31DEC26/USDC | …#YES, …#NO |
| Perp | BTC/USDSO:USDSO | BTC/USDSO:USDSO |
| Categorical (soon) | US-ELECTION-28/USDC | …#TRUMP, …#NEWSOM, … |
Binary symbols are synthesized as ASSET-STRIKE-DDMONYY/QUOTE (quote = the
collateral's ERC-20 symbol), with an -HHMM expiry component for intraday
series markets (ETH-171780-03JUL26-0930/TUSDC); any residual collision gets
a deterministic 4-hex market-id tiebreaker on the base side of the slash. Everything before # follows standard exchange symbol conventions; #OUTCOME
is our extension (binary markets need one). Any raw ref works wherever a symbol does — a pool address,
market id, or BinaryMarket address resolves to the same tradable (outcome
markets default to #YES).
Numbers are human units in the tradable's own terms: a #NO price of 0.38
is the NO probability — the YES-terms complement, raw integers, and escrow
math are all internal. Raw payloads ride on every struct's info.
Verbs
| Method | Backed by | Notes |
|---|---|---|
loadMarkets() / fetchMarkets() | indexer (+ one-time symbol() reads) | builds the symbol registry |
fetchOrderBook(symbol, limit?) | chain | head-fresh one-shot |
fetchTrades(symbol, since?, limit?) | indexer | public tape |
fetchOHLCV(symbol, "5m", since?, limit?) | indexer | 1m 5m 15m 1h 4h 1d |
fetchOpenOrders(symbol?) · fetchMyTrades(symbol?) | indexer | authenticated; lags chain |
fetchBalance() | chain | wallet balances by currency code (incl. binary YES/NO ERC-6909 holdings, keyed by tradable symbol) |
fetchStatus() | local | watch/socket health |
watchOrderBook(symbol, limit?) | local store | zero RTT, current to last block |
watchTrades(symbol) · watchOrders(symbol) · watchMyTrades(symbol) | local store | streaming |
createOrder(symbol, type, side, amount, price?, params?) | chain write | 1-RTT confirm, fills decoded |
cancelOrder(id, symbol) | chain write | |
mintSet / burnSet / redeem (symbol, amount) | chain write | outcome markets only |
fetchFundingRate(symbol) | chain | perps: mark/index + funding rate |
fetchPositions(symbols?) | chain | perps: open MarginBank positions |
depositMargin / withdrawMargin (symbol, amount) | chain write | perps: cross-margin collateral |
watchPrice(asset) · fetchPrice(asset) | price feed | index price (EMA oracle) — see Price feeds |
fetchPriceOHLCV(asset, "1m") | price feed | index candles: 1m/1h/1d |
market(ref) | local | resolve any handle to its tradable |
close() | local | release all watches |
Still reserved: watchPositions, setLeverage (the raw
trader.setPerpLeverage exists for the latter).
The watch* idiom
Each await resolves when the channel next changes; the first call resolves
immediately after the (ref-counted) market watch hydrates:
while (running) {
const book = await exchange.watchOrderBook(symbol, 5); // resolves per change
requote(book.bids[0], book.asks[0]);
}
"Did my resting order fill?" — watch your orders and look for the status flip:
const orders = await exchange.watchOrders(symbol); // resolves per change
const mine = orders.find((o) => o.id === orderId);
if (mine?.status === "closed") … // filled
createOrder params
type: "limit" | "market"— market orders compute a crossing limit from the best opposite level ±params.slippage(default 1%) and send IOC; they throw if the opposite side is empty.params.timeInForce: "GTC" | "IOC" | "FOK" | "PO"(orpostOnly: true) — maps to the on-chain order types.params.builder/params.builderFeeBpsTimes1k(binary only) — attribute the order to a routing/builder frontend and pay it a per-order fee (pool bps×1000 unit). Requires a prior one-timeexchange.trader.approveBuilder({ pool, builder, maxFeeBpsTimes1k })opt-in on the pool, else a non-zero fee reverts. Ignored on spot/perp.- The returned order:
status: "open"(rested),"closed"(fully filled in the same round-trip —filled/remainingsay how much), or"canceled"(IOC remainder).infocarries the receipt + raw fills.
Structs
One set of shapes throughout: markets { id, symbol, type, base, quote, active, precision, limits, outcomes?, info }; books as [price, amount] pairs, best
first; orders { id, symbol, type, side, price, amount, filled, remaining, status, timestamp, info }; trades { id, symbol, price, amount, cost, side?, timestamp, info }; balances { [code]: { free, used, total } } (escrowed
funds live in the pools, so wallet free === total); OHLCV rows
[ms, open, high, low, close, baseVolume].
How each market kind maps
- Spot — symbol is the tradable;
sideis base-buy/base-sell; done. - Binary — two tradables per market. The engine keeps one YES-terms book;
the resolver presents each outcome's own view (prices, sides, candles all
outcome-relative).
mintSet/burnSet/redeemoperate at market level. - Perps —
type: "swap", settle = quote (linear, collateral-settled);createOrderunchanged (margin locks from your MarginBank balance —depositMarginfirst);fetchPositions/fetchFundingRateread the MarginBank/pool live. - Categorical / multi-outcome (teed up) — N tradables on one pool
(outcome index = the order's
userData, exactly as binary uses 0/1 today); same verbs, and cross-book complete-set fills will surface inwatchMyTradeswith a shared match id ininfo.
The design intent: the verbs never change again — every future market kind
is new data (a type, an outcomes[] list), not new API.