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:
| Tier | Methods look like | Backed by | Cost & freshness |
|---|---|---|---|
| Live store | getLive* (synchronous) | in-memory materialization of watched markets | zero round-trips, current to the last block |
| Chain | get*Onchain, getBinaryOrderBook, balances | eth_call over the socket | one round-trip, current to head |
| Indexer | list*, 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: truealso watches the creation events (theMarketCreatorfactory ANDBinaryMarketsModule.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'sgetOutcomeBalances).getErc20Metadata(token)/getErc20Allowance(token, owner, spender)— token name/symbol/decimals and a spender allowance (gate anapproveon it).getContractMeta(address, { proxy })— contract/proxy introspection; andgetMaxVenueFeeBps()— the protocol's hard fee-rate cap (bps).- Perp margin health —
getMarginAccount(now includingimReq/mmReq/cmReq/marginStatus),getAccountHealth, andgetLiquidationPrice(allMarginBankreads); see perps. getVaultBalance(vault, owner, token)— the LIVE claimable balance behind the append-onlygetVaultPayoutFallbackscredit 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-narrowedlistBinaryMarkets/listLiveBinaryMarkets/listPastBinaryMarkets/getBinaryMarket. - History:
getCandles(OHLCV, chart-ready),getFills, and the account-scopedgetOrders(owner, opts)(an owner's order history →OrderRow[]),getUserFills(account, { pool })(an account's fills →FillRow[]), andgetMarketStatusHistory(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 withdecodeBinaryVenueFeeParams). - Fee / resolution / router history (binary):
getMarketResolution,getRouterActions,listProtocolFees/listBuilderFees/listSettlementFees(per-fill streams behindgetMarketFees, filterable bypayer),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 totalscountMarkets/countBinaryMarkets/countVenues/countOperators(each takes the same filter as itslist*sibling — use for pagination headers).CANDLE_INTERVALSis the exported bucket listgetCandlesaccepts. - Ops:
getSyncStatus— how far behind the indexer itself is.
Two of these have faster live twins a trading loop should prefer:
getOpenOrders → getLiveUserOrders, getFills → getLiveFills.
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.