{s}omniamarkets

The engine (advanced)

This guide tours the engine tier — the raw SomniaMarketsClient: bigint-exact, address-keyed. It is not a second entry point: you reach it through the exchange —

const exchange = new SomniaMarkets({ indexerUrl, chain, wsRpcUrl });
const client = exchange.client;   // the engine: exact raw-unit reads + watches
const trader = exchange.trader;   // the raw write tier (needs a signer)

Most applications stay on the exchange API itself; the engine is the tier for exact escrow math, custom reads, and anything the exchange verbs don't cover. Both surfaces share one config, one socket, one watch/store. Multi-chain deployments are multiple exchanges: one per chain. For market-kind specifics, see the binary markets and spot guides.

The three read tiers

Every read on the client belongs to one of three tiers. The method name tells you which — and the tier tells you the freshness and the cost:

TierMethods look likeBacked byCost & freshness
Live storegetLive* (synchronous)in-memory materialization of watched marketszero round-trips, current to the last block
Chainget*Onchain, getBinaryOrderBook, balanceseth_call over the socketone round-trip, current to head
Indexerlist*, getCandles, getPortfolio, …GraphQL (HTTP)one round-trip, lags the chain slightly

Rule of thumb: anything that gates an action reads the live store or the chain; the indexer is for history and aggregates. Indexer reads throw on request failure — an empty result always means "no rows", never "it broke".

Watches — scoped, ref-counted live data

Nothing is streamed by default. You open a watch on what you care about, and the client becomes a local indexer of exactly that scope:

const watch = await client.watchMarket(pool);  // one market: book, fills, orders, status
const book  = client.getLiveBinaryOrderBook(pool);   // synchronous, zero round-trips, last-block fresh
// ... trade against it ...
watch.stop();                                  // release (ref-counted, brief linger)
  • watchMarket(pool) — hydrates a consistent snapshot of that market (its row, recent fills, full resting book) and streams its events from the chain WebSocket. Resolves once reads are live. This is the right default: cost scales with what you watch, not with the protocol.
  • watchMarkets({ discover? }) — everything the indexer knows, for list views and multi-market bots; discover: true also watches the creation events (the MarketCreator factory AND BinaryMarketsModule.createMarket) so newly created markets — rolling-series or module-created — join live.
  • watchUser(user) — hydrates one account's order/fill history (live events are attributed to every account automatically within watched markets).
  • getWatchStatus(pool)"unwatched" | "hydrating" | "live"; this is how you distinguish "the book is empty" from "I'm not watching this pool".

Watches are ref-counted: two watchers of the same pool share one snapshot and one subscription, and a released scope lingers ~30s before teardown so quick re-watches don't re-snapshot. If the socket drops, watches heal themselves — resubscribe with backoff, then backfill the missed blocks straight from chain (the indexer is only ever touched to hydrate a new scope).

While a market is watched, these answer synchronously with zero round-trips: getLiveBinaryOrderBook / getLiveSpotOrderBook (the resting book — the one to render or quote against), getLiveFills (tape), getLiveUserFills / getLiveUserOrders (one account's activity), getLiveMarkets / getLiveMarketByPool / getLiveMarketByAddress (market rows with live status + stats), and getLiveStatus (global health).

Reactivity without React: subscribeLive fires after every batch of changes — re-read whatever you need inside the callback:

const watch = await client.watchMarket(pool);
client.subscribeLive(() => {
  const { yesBids, yesAsks } = client.getLiveBinaryOrderBook(pool, { depth: 5 });
  requote(yesBids, yesAsks); // runs within ms of the on-chain event
});

React

Provide the client once; the hooks read it from context and watch automatically — rendering a market's book is what subscribes to it, and unmounting releases it:

import { SomniaMarketsProvider, useLiveBinaryOrderBook, useLiveFills, useWatchUser } from "@somnia-chain/markets-sdk/react";

function App({ children }) {
  return <SomniaMarketsProvider client={client}>{children}</SomniaMarketsProvider>;
}

function Terminal({ pool, account }: { pool: string; account?: string }) {
  useWatchUser(account);                     // hydrate the user's history once
  const book = useLiveBinaryOrderBook(pool, 11);   // auto-watches `pool` while mounted
  const tape = useLiveFills(pool, 40);       // shares the same watch (ref-counted)
  // ...
}

The full set: useLiveBinaryOrderBook, useLiveSpotOrderBook, useLiveFills, useLiveUserFills, useLiveUserOrders, useLiveMarketByPool, useLiveMarketByAddress, useLiveStatus, useIsTailing, plus the explicit useWatchMarket / useWatchUser and useSomniaMarketsClient. Every pool-keyed data hook holds a ref-counted watch on its pool while mounted; useWatchMarket also returns the pool's WatchStatus for loading UI.

For the indexer tier (history + directories, not the live store) there is a generic engine hook — useIndexerQuery(fn, deps) runs fn(client) on mount and whenever deps change, tracks { data, loading, error, refetch }, and discards stale responses:

const { data: markets } = useIndexerQuery((c) => c.listBinaryMarkets({ limit: 20 }), []);

Concrete wrappers over the common reads: usePortfolio, useMarkets, useCandles, useMarketFees, useOperators. Separately, useLiveMarkets is the zero-round-trip live-store view of every watched market (pair it with a discovery watch), not an indexer fetch.

Chain reads — head-fresh, no watch required

One eth_call round-trip over the client's socket (concurrent calls pipeline):

  • getMarketOnchain — a BinaryMarket's full wiring + state. Authoritative for write eligibility (status, resolution), and works before the indexer has ever seen the market.
  • getBinaryOrderBook / getSpotOrderBook — the book from the contract; the one-shot fallback (or checksum) for the live variants.
  • getErc20Balance / getNativeBalance — raw balances (use these, not indexer balances, to gate a write).
  • getBalances(tokens, account) — batch multi-token balance read (one round-trip for a whole wallet); getOutcomeBalance(outcomeToken, account, id) — a single ERC-6909 outcome-id balance (the on-chain point read behind the indexer's getOutcomeBalances).
  • getErc20Metadata(token) / getErc20Allowance(token, owner, spender) — token name/symbol/decimals and a spender allowance (gate an approve on it).
  • getContractMeta(address, { proxy }) — contract/proxy introspection; and getMaxVenueFeeBps() — the protocol's hard fee-rate cap (bps).
  • Perp margin health — getMarginAccount (now including imReq/mmReq/cmReq/ marginStatus), getAccountHealth, and getLiquidationPrice (all MarginBank reads); see perps.
  • getVaultBalance(vault, owner, token) — the LIVE claimable balance behind the append-only getVaultPayoutFallbacks credit log (vault credits emit no event, so the current balance must be read from chain).
  • getHeadBlock, getStopOrderSomiPayment, getSystemInfo.

Indexer reads — history and aggregates

One GraphQL round-trip; these are the only methods that touch the indexer (besides a watch's initial snapshot):

  • Markets: listMarkets, getMarket, and the binary-narrowed listBinaryMarkets / listLiveBinaryMarkets / listPastBinaryMarkets / getBinaryMarket.
  • History: getCandles (OHLCV, chart-ready), getFills, and the account-scoped getOrders(owner, opts) (an owner's order history → OrderRow[]), getUserFills(account, { pool }) (an account's fills → FillRow[]), and getMarketStatusHistory(marketId) (a market's lifecycle transitions).
  • Wallet views: getPortfolio (binary positions + orders + trades in one round-trip), getSpotPortfolio, getSpotStopOrders, getOpenOrders, getOutcomeBalances.
  • Control plane: listOperators / getOperator / listVenues / getVenue — the indexed MarketsCore operator/venue directory (venue ids are opaque bytes32; decode a BINARY_V1 venue's fee bytes with decodeBinaryVenueFeeParams).
  • Fee / resolution / router history (binary): getMarketResolution, getRouterActions, listProtocolFees / listBuilderFees / listSettlementFees (per-fill streams behind getMarketFees, filterable by payer), listBuilderApprovals, getVaultPayoutFallbacks — see binary markets.
  • Perp account plane: getFundingPayments, getMarginEvents, getLiquidations, getFundingRateHistory, getOpenInterestHistory — the append-only history the chain doesn't expose; see perps.
  • Lookups + totals: getMarketByPool (resolve a market by pool address), countOrders / countUserFills (history-page totals), and the board totals countMarkets / countBinaryMarkets / countVenues / countOperators (each takes the same filter as its list* sibling — use for pagination headers). CANDLE_INTERVALS is the exported bucket list getCandles accepts.
  • Ops: getSyncStatus — how far behind the indexer itself is.

Two of these have faster live twins a trading loop should prefer: getOpenOrdersgetLiveUserOrders, getFillsgetLiveFills.

Writes — createTrader

const trader = client.createTrader({ privateKey }); // or { walletClient } in the browser
const { orderId, fills } = await trader.placeOrder({ pool, side: "BUY_YES", price, quantity });

createTrader binds a signer to this client's chain, fees, and socket and returns a Trader. The trader is independent of the watches — a pool's escrow tokens resolve from the pool contract itself (one cached read) when you don't pass them. With a local key the SDK signs locally — fixed fees, locally-tracked nonce, zero pre-send RPCs — and confirms in one round-trip via realtime_sendRawTransaction; every write resolves only once mined, with its receipt (and, for orders, the decoded orderId + fills). Market-kind specifics: binary markets, spot.

For the MarketsCore control plane there is a second, low-frequency write tier: client.createOperatorAdmin({ privateKey }) returns an OperatorAdmin with registerOperator, updateOperator, createVenue (venues are typed by a bytes4 market-type id — MARKET_TYPE_BINARY_V1 — and get a contract-generated bytes32 venue id back), updateVenue, setVenueEnabled, and the two-step operator-ownership transfer. Build a BINARY_V1 venue's fee bytes with client.encodeBinaryVenueFeeParams({...}).

Escape hatch

publicClient exposes the underlying viem WebSocket client for any contract read the SDK doesn't cover — same socket, same pipelining.


Next: binary markets, spot, perps, and the architecture guide for how the machine works inside.