{s}omniamarkets

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:

KindMarket symbolTradables
SpotSOMI/USDCSOMI/USDC
BinaryBTC-95000-31DEC26/USDC…#YES, …#NO
PerpBTC/USDSO:USDSOBTC/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

MethodBacked byNotes
loadMarkets() / fetchMarkets()indexer (+ one-time symbol() reads)builds the symbol registry
fetchOrderBook(symbol, limit?)chainhead-fresh one-shot
fetchTrades(symbol, since?, limit?)indexerpublic tape
fetchOHLCV(symbol, "5m", since?, limit?)indexer1m 5m 15m 1h 4h 1d
fetchOpenOrders(symbol?) · fetchMyTrades(symbol?)indexerauthenticated; lags chain
fetchBalance()chainwallet balances by currency code (incl. binary YES/NO ERC-6909 holdings, keyed by tradable symbol)
fetchStatus()localwatch/socket health
watchOrderBook(symbol, limit?)local storezero RTT, current to last block
watchTrades(symbol) · watchOrders(symbol) · watchMyTrades(symbol)local storestreaming
createOrder(symbol, type, side, amount, price?, params?)chain write1-RTT confirm, fills decoded
cancelOrder(id, symbol)chain write
mintSet / burnSet / redeem (symbol, amount)chain writeoutcome markets only
fetchFundingRate(symbol)chainperps: mark/index + funding rate
fetchPositions(symbols?)chainperps: open MarginBank positions
depositMargin / withdrawMargin (symbol, amount)chain writeperps: cross-margin collateral
watchPrice(asset) · fetchPrice(asset)price feedindex price (EMA oracle) — see Price feeds
fetchPriceOHLCV(asset, "1m")price feedindex candles: 1m/1h/1d
market(ref)localresolve any handle to its tradable
close()localrelease 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" (or postOnly: 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-time exchange.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/remaining say how much), or "canceled" (IOC remainder). info carries 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; side is 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/redeem operate at market level.
  • Perpstype: "swap", settle = quote (linear, collateral-settled); createOrder unchanged (margin locks from your MarginBank balance — depositMargin first); fetchPositions/fetchFundingRate read 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 in watchMyTrades with a shared match id in info.

The design intent: the verbs never change again — every future market kind is new data (a type, an outcomes[] list), not new API.