Somnia Markets / Reference
Read tiers
This reference lists the sources used by SDK reads, watches, and status accessors. Use it to budget startup dependencies and requests, and to decide whether a value is fresh enough for your application. Architecture explains transport ownership.
The tiers
| Tier | Source and freshness | Startup requirements | Cost |
|---|---|---|---|
| Market live store | Main-indexer snapshot, or a chain seed on an opted-in watch, then applied chain events. Values reflect the last applied events, not a guarantee of the current head. | A market watch hydrates from the main indexer by default, and needs chain RPC for subscriptions and backfill. watchMarket(pool, { seed: "chain", marketId }) seeds the book from the pool contract instead, so it starts with the main indexer unreachable. A user watch hydrates indexed history; live updates require watched markets. |
Synchronous selectors make no requests. Cold hydration, backfill, ongoing subscriptions, and recovery have network cost. |
| Chain | Chain RPC, normally over the owner's WebSocket. Reads reflect the blocks served by the node; a group of reads need not share one block. | A chain WebSocket endpoint and any method-specific contract addresses. | One or more RPC calls, sometimes cached or batched. Parallel calls do not guarantee one round-trip. |
| Main indexer | Envio/Hasura GraphQL over HTTP. Values reflect indexed state and can lag the chain. | The main indexer endpoint; aggregate reads can require privileged headers. The live account view also needs the indexer's GraphQL WebSocket endpoint. | One or more HTTP requests, depending on pagination and composition. No fixed freshness delay is guaranteed. |
| Price-feed indexer and live price store | Separate price-feed Hasura service. HTTP reads and subscription updates reflect its indexed prices, not direct chain prices. | Price-feed HTTP configuration; watches also need its Hasura WebSocket endpoint. | Fetches make HTTP requests. Watches hydrate over HTTP in batched snapshot requests and share one Hasura socket per client and endpoint. Store selectors are synchronous and make no requests. |
indexerUrl is optional: a caller that only uses chain reads (smart accounts included) constructs without one, and the main-indexer methods below then throw NotConfiguredError. Configuring it does not mean that every method requests the main indexer. The chain socket opens lazily on chain I/O. Price-feed transport and failures are separate from the market store.
A result can combine tiers with different timestamps. The SDK does not generally compare indexed state with the current chain head to certify freshness. Check the method-specific inputs below before treating a derived value as current.
Return contract
- An indexer point read resolves to
nullwhen the row does not exist and throwsIndexerErrorwhen the request fails. - An indexer list read resolves to
[]when there are no rows and throwsIndexerErrorwhen the request fails. - A chain read throws on failure. It never returns
nullin place of a failure. - A live-store read returns empty data for a market with no active watch.
getWatchStatus(pool)is"unwatched"in that state. - The live store keeps market rows in bounded memory. It always keeps a market that a pool is bound to or that a retained fill or order names. It keeps up to 500 other market rows (released or replaced markets) and evicts the earliest-discovered ones first. An evicted market is absent from
getLiveMarketsandgetLiveMarketByAddress, and a late settlement event for it does not change the live store. Read it from the indexer (getMarket,getBinaryMarket). - A chain-seeded watch loads no fill history, so
getLiveFillscan be empty while the book is complete.getLiveStatus().fillsLoadedseparates the two states:falsemeans the history was never loaded,truemeans there are genuinely no fills in the loaded range. A failed fill read setsfailure.operationto"fill-history"and does not empty the book. - Orders a chain seed writes carry an empty
createdAtandtxHash: placement history is indexer-only and the seed does not invent it.userOrderssorts oncreatedAt, so seeded orders sort LAST behind any order carrying a real placement timestamp. A chain-seeded pool is not re-snapshotted while the watch lives, so they stay undated for the store's lifetime; read placement history from the indexer. - The market row a chain seed writes carries only what the module answers. Indexer-only fields —
question,asset,oracleQuestion,strike/mode,tradingStart,intervalSec/interval,createdAtTimestamp/createdAtBlock,createdByTx, the volume counters, and the origin attribution — are empty,nullor zero and STAY that way: a seeded pool is not re-hydrated by a later watch, and the row outlivesstop()until cold-market eviction drops it. Read the indexer directly (getBinaryMarket) when those fields are needed.strike/modearenullrather than"0"/"reference", because a strike of 0 is the protocol's sentinel for an up/down market and would mislabel a fixed-strike one. - A chain seed never blanks a row the indexer already supplied. Inside the cold-market allowance the row outlives
stop(), so a pool watched from the indexer first and chain-seeded later keeps each indexer-only field listed above, while every field the module answers —status,backing,nonce,finalized,voided,winningOutcome,collateral, the decimals and the token ids — takes the fresher chain value. getLiveStatus()reportsseedSource("indexer","chain", ornullbefore the first watch),seedBlock, andseedTruncatedfor the MOST RECENT seed only. With several watched pools those values cannot be attributed to a pool: a later seed overwrites them, andfillsLoadedturns true when any pool's fill lane lands. ReadgetSeedStatus(pool)for one pool's own four fields; it isnullwhen that pool has no hydrated watch. The indexer snapshot applies a PER-SCOPE order budget that is SHARED across the scope's pools, not a per-pool floor, and it drops the oldest resting orders first. A chain seed has no such cap;seedTruncatedistrueonly when the drain reaches its own page budget.watchMarketsselects markets by lifecycle, not by age. It hydrates every SPOT and PERP market and every BINARY market that is not finalized. A finalized BINARY market is left out of the watch. Read it withgetBinaryMarket(id).watchMarket(pool)applies no lifecycle filter, but it hydrates the market the pool serves now, so it hydrates a finalized market only until the pool is recycled. One snapshot hydrates at most 500 eligible markets, newest first. When more eligible markets exist,getLiveStatus().allMarketsTruncatedistruewhile the all-markets watch is active, and the oldest eligible markets are not watched. Only the all-markets watch writes that field, so a later pool or chain seed does not reset it.- A live price read keeps answering with its last values after the server rejects the asset's subscription.
getPriceStatus(asset)is"error"in that state, and the rejection is also emitted as awarnevent on the debug channel.
Exchange API (SomniaMarkets)
| Method | Tier |
|---|---|
loadMarkets, fetchMarkets |
Indexer, plus chain reads for perp discovery and token symbols. Neither source's failure throws: the registry read reports through registryReadError and perp discovery through perpDiscoveryError, so one source being down still returns the other's markets. |
fetchOrderBook |
Chain |
fetchTrades, fetchOHLCV, fetchOpenOrders, fetchOrders, fetchMyTrades, fetchOpenStopOrders, fetchPortfolioAnalytics, fetchFundingRateHistory |
Indexer |
fetchTicker |
Cached market metadata and main-indexer candles; on perps, also best-effort chain state. An EMPTY candle window falls last back to the cached market row, which stamps lastSource from that row's lastTradeAt; lastSource is absent only when last itself is absent or that timestamp is null. A candle request FAILURE is CONTAINED: statsState becomes "unavailable", every 24h field is absent, and the call still resolves so the perp chain numbers survive. A market with indexed: false reports the same state and sends no candle request. statsAsOf carries the 24h window's own as-of; timestamp remains response time, not source freshness. A failed perp state read leaves candle-derived data and reports perpState: "degraded". |
fetchBalance |
Loads market/currency metadata through loadMarkets, then reads chain token/native balances and main-indexer binary holdings. Cost depends on currencies and metadata discovery. |
fetchFundingRate |
Chain |
fetchPositions |
Chain per position, but the call first awaits loadMarkets, so it depends on a loaded perp registry on the first call and on every call while the registry is empty. That registry is an INDEXER dependency only when no addresses.perpPoolFactory is configured: with one, loadMarkets discovers the perps from the factory on chain. Called with no refs, it returns positions only for the perps the registry loaded: a short perp set gives a silently short result, not an error. Explicit refs do not remove the dependency — loadMarkets is awaited either way — but they turn a silently short result into a loud InvalidInputError for a symbol the registry never loaded. |
fetchPrice, fetchPriceOHLCV |
Separate price-feed Hasura HTTP |
market, priceToPrecision, amountToPrecision |
Local |
watchOrderBook, watchTrades, watchOrders, watchMyTrades |
Live store. The first call opens the market watch. |
watchPrice |
Live price store. The first call opens the price watch. |
A watch* call resolves with the current value on its first call and on every later call resolves when the channel's value changes.
unwatchMarket(symbol) releases one market's watch and its channels. A pending watch* call on that market rejects with a DOMException named AbortError. The next watch* call opens a fresh watch.
Engine (SomniaMarketsClient)
The tables in this section are the canonical Engine inventory. Names refer to exchange.client (or exchange.engineFor(chainId) — exchange.client names no chain and throws InvalidInputError on an exchange spanning more than one), not similarly named Exchange methods. The inventory guard checks membership only; source and freshness claims require source review. Configuration, cleanup, escape hatches, and capability factories are outside this read inventory.
Live store
| Group | Methods |
|---|---|
| Market selectors and status | getLiveMarkets, getLiveMarketByPool, getLiveMarketByAddress, getLiveFills, getLiveUserFills, getLiveUserOrders, getLiveFundingUpdates, getLiveBinaryOrderBook, getLiveBinaryOrderBookByMarket, getLiveSpotOrderBook, getLiveStatus, getSeedStatus, getWatchStatus, isTailing, subscribeLive, listLiveLiquidations, listLiveFundingPayments, getPerpPlaneStatus |
| Price feed | getLivePrice, getLivePrices, getLivePriceTicks, getLivePriceFeedInfo, getPriceStatus, subscribePrices |
| Watches | watchMarket, watchMarkets, watchUser, watchPrice, watchPrices, watchPerpMarkets |
getLiveUserOrders keeps every unexpired open order, up to 400 recent
unreferenced terminal orders per pool, and terminal orders needed by retained
fills. Use getOrders for older order history. On-chain expiry emits no event,
so each chain head is the retention checkpoint for expired orders. At the first
head whose timestamp is strictly after an Open order's expiry, that order
counts toward the 400-row cap. It keeps status Open: the chain holds its
escrow until a cancel or a sweep, so removal from the live store does not mean
a refund. An active watchUser keeps every expired open order of its user,
including orders placed after the watch started, until the watch is released.
A later cancel or sweep event for a removed order changes nothing. Accepted indexed Closed orders count toward the cap at
hydration. Live Closed orders wait for a later block from the same pool so a
delayed same-block OrderRested can still make them open.
Chain
| Group | Methods |
|---|---|
| Books and orders | getBinaryOrderBook, getSpotOrderBook, getOrderOnchain, getBookTopsOnchain, getOwnOpenOrdersOnchain, getOpenOrdersOnchain, getAllOpenOrdersOnchain |
| Markets and system | getMarketOnchain, getPoolMarketBinding, getMarketIdOnchain, getContractMeta, getHeadBlock, getSystemInfo, getTransactionSummary, getPoolCreator, getFreePools, getOnchainResolutionPrice, getMaxVenueFeeBps, encodeBinaryVenueFeeParams |
| Balances and allowances | getErc20Balance, getErc20Metadata, getErc20Allowance, getBalances, getNativeBalance, getOutcomeBalance, getVaultBalance, getOwnLockedBalance, getLockedTokenBreakdown |
| Operators and builders | getOperatorPermissionsRegistry, isOperatorAuthorized, isGloballyApproved, isApprovedForPool, getMaxBuilderFeeBpsTimes1k, getBuilderApproval, getEffectiveBuilderApproval, getStopOrderSomiPayment, getPerpStopOrderSomiPayment, getPerpStopOrder, getPerpStopOrders, getUnclaimedPerpStopSomi, getManualVaultMode, getAutoPullRequirement, convertToQuoteAtPriceCeil |
| Perps | getPerpState, getPerpFeedStatus, getPerpPosition, getMarginAccount, getAccountHealth, getAccountEquity, listAccountEquities, getLiquidationPrice, previewPerpLiquidationPrice, getPerpLeverage, getPerpPositionAnalytics, listPerpPositionAnalytics, getMaxPerpOrderSize, previewPerpOrderMargin, previewPerpClosePnl, getPerpSideHolders, getBankruptcyPrice, getPerpSystemConfig, getInsuranceFundState, getLiquidationEngineConfig, tryGetPerpAccountEquity, getPerpCollateralBasis, listPerpPoolStatuses, listTradeablePerpPools, isPerpPoolRegistered, getPerpRiskParams, getPerpHealthSnapshot, getEffectiveImfBps, getPerpMaxLeverage, getPerpLeverageImSurcharge, tryGetPerpLeverageImSurcharge, readPerpMarketFromChain, quotePerpFundingPayer, getPerpMainFunding, getPerpLinkedWalletRegistry, getPerpWalletPullCapacity, getPerpWalletLinkage, listPerpLinkedChildren, getPerpMaxLinkedChildren, getPerpVoucherCampaign, getPerpVoucherEligibility, getPerpVoucher, getPerpFundingPremium, meetsPerpImForFill, quoteMeetsPerpImForOrder, quotePerpOrderTopUp |
| Oracle hub | getSchedulingCost, earmarkedOf, creditOf, outstandingOf, withdrawableOf, payerCreditOf, payerOf, resolveReserve, quoteCreateMarketValue |
| SomniaLend | client.lend.listReserves, client.lend.getAccount |
| Smart accounts | client.accounts.find, client.accounts.nextFreeIndex, client.accounts.ownersOf, client.accounts.custodyChain, client.accounts.authorityPath, client.accounts.tree, client.accounts.holdings, client.accounts.health, client.accounts.buildExecution |
Indexer
| Group | Methods |
|---|---|
| Markets | listMarkets, listRegistryMarkets, listRegistryMarketsChecked, countMarkets, countMarketsBounded, getMarket, getMarketByPool, listMarketsByPool, listBinaryMarkets, listLiveBinaryMarkets, listPastBinaryMarkets, getBinaryMarket, getBinaryMarketByAddress, listBinaryVenueIds, listBinaryAssets, countBinaryMarkets, countBinaryMarketsBounded, listSpotMarkets, getSpotMarket, listPerpMarkets, getPerpMarket, getMarketFees, getMarketStatusHistory |
| History | getCandles, getFills, getFill, getUserFills, countUserFills, getOrderFills, getOrder, getOrders, getOpenOrders, countOrders, getBookTops, listSweepableOrders, getMarketActivity, getTradeContext, getRouterActions, getUserFillsPage, getUserFillHistory, getFillsPage, getLatestFillBlock, getLatestActiveBlock |
| Portfolios | getPortfolio, getSpotPortfolio, getPerpPortfolio, getOutcomeBalances, getSpotStopOrders, listPerpStopOrders, listPerpOrderHistory, listPerpPositions |
| Resolution | getMarketResolution, listMarketResolutionEvents, listMarketReferenceLinks, getOpeningPrices, getResolutionPrices |
| Fees and perps history | listProtocolFees, listBuilderFees, listSettlementFees, listBuilderApprovals, getVaultPayoutFallbacks, listVaultPayoutFallbacks, getFundingPayments, getMarginEvents, listLiquidations, getLiquidations, listFundingRateHistory, listFundingRateCandles, listPerpFees, getOpenInterestHistory, getFundingRateHistory, listPerpOrderRejections, listPerpPositionChanges, listPerpPositionChangesForFills, listPerpInsuranceFundEvents, listPerpWalletLinkEvents, listPerpMarginPulls, listPerpMainFundingEvents |
| Control plane | listOperators, getOperator, countOperators, listVenues, getVenue, countVenues, listMarketCreators, getMarketCreator, listOracleAdapters, getOracleAdapter, listSeries, listSeriesLatestMarkets, getSeries, getOracleQuestion, listOracleQuestions, getOperatorHubAccount, listOperatorHubAccounts, listOracleBinds, listOracleCallbacks, getSyncStatus, getPool, listPoolBindings |
| Smart accounts | client.accounts.getIndexedAccount, client.accounts.listDerivedAccounts, client.accounts.listControlledAccounts, client.accounts.listOwnerHistory, client.accounts.getActivityPage, client.accounts.getIndexerStatus, client.accounts.listModuleHistory, client.accounts.getCreationAuthorization, client.accounts.getFactoryState, client.accounts.getAccountsPage, client.accounts.getNetworkActivityPage, client.accounts.getFactoryHistoryPage |
Price-feed HTTP reads
| Group | Methods |
|---|---|
| Current prices and feed metadata | fetchPrice, fetchPrices, fetchPriceFeedInfo, listPriceFeeds |
| Price history | fetchPriceHistory, fetchPriceCandles |
Mixed and derived reads
| Inputs and behavior | Methods |
|---|---|
| Main indexer plus a mandatory chain block timestamp lookup. The timestamp anchors indexed activity; it does not make indexed rows current. | getBlockActivity, getAdjacentActiveBlocks |
| Main-indexer activity. Chain receipt and block lookups run only when no indexed event and no caller-supplied anchor place the transaction. Failure to obtain that anchor leaves the indexer order probe available. | getTransactionActivity |
| Synchronous live binary book and stored market decimals. Needs a hydrated market watch for useful depth. | quoteBinaryOrder |
| Live binary book plus cached chain tick/lot grid. A market-id target can add an indexer lookup when the store cannot resolve its pool. An empty live book is not replaced by an indexer book. | quoteBinaryStake, quoteBinarySell |
| Chain grid read cached per pool for the client lifetime. Later administrative grid changes do not invalidate this cache. | getBinaryBookParams |
| Chain closing-price view, including its capture state. | getClosingPrice |
| Main-indexer candles; a market-id target also resolves its pool through the main indexer. | getMarketStats24h |
| Main-indexer market metadata, fills, router actions, and outcome balances, plus chain book top for the mark clamp. Without configured chain access, uses indexed last price alone; an operational chain failure propagates. | getBinaryPositionPnL |
| Main-indexer positions and market metadata, fills, and router actions, plus chain book tops for the mark, read at one pinned block. Without configured chain access, uses indexed last price alone; an operational chain failure propagates. This does not use the live book. | getOpenPositionsWithPnL |
| Main-indexer positions and resolution data, plus indexed settlement fees for winning markets. This estimates claimable amounts from indexed state. | getClaimable, getClaimablePage |
Main-indexer account view of event-contract positions, as a guarded snapshot: two version reads bracket the row read, and the rows count only when the version did not move. Concurrent snapshot callers for one account share one read with independent cancellation, and a ready live view is reused without a request. The version certifies completeness, not freshness against the chain head. Until the indexer enables the view on a chain, the result is unavailable. |
client.accountOutcomePositions.getSnapshot |
The same view, followed live: successful reads poll the account's version every 5 s and repeat the guarded snapshot when it moves. Recoverable failures retry after 5, 10, 20, 40, then 60 s, capped at 60 s. Unavailable capability rechecks use HTTP every 5 minutes, without the orders view's capability subscription. A missing indexer endpoint is terminal (failed.retrying: false). Recovery restores the 5 s cadence. Held data's clock-derived status updates are checked every 5 s during backoff. tooLarge watches the version without rereading unchanged oversized rows. The reader remains active for 30 s after the last unsubscribe; stopLive() or owner cancellation ends it immediately, including during reuse, and active subscribers receive a final closed view. |
client.accountOutcomePositions.subscribe |
| Independent RPC head, then main-indexer metadata plus processed-block time. Raw measurements, no SDK stale threshold. An indexer more than 10,000 blocks past the head follows an earlier genesis of the chain id, so the read throws. | getIndexerFreshness |
| Main-indexer metadata, polled until the target block. For the client's own chain, one RPC head read when the wait would resolve; an indexer more than 10,000 blocks past that head rejects the wait. Without configured chain access, the head read is skipped. | waitForIndexedBlock |
| One independent head for response-associated reads, using the same owner. | createObservedReads |
| Per-asset price watch lifecycle, without I/O. | getPriceHealth, listPriceHealth |
| Main-indexer account view, as a guarded snapshot: two version reads bracket the row read, and the rows count only when the version did not move. The version certifies completeness, not freshness against the chain head. Stop venues come from main-indexer market rows; a perp LIMIT stop's limit price comes from its chain registry at the head, so that read opens the chain socket when it is closed. It never sends indexerHeaders. | client.accountOrders.getSnapshot |
| Main-indexer account view, followed live: one guarded snapshot, then the account's change stream over one indexer GraphQL WebSocket per client. After 10 s with no frame on that socket the client pings the indexer, and it closes the socket when nothing answers within 10 s more; the deployed indexers ping every 5 s, so the client normally sends none. Each batch of changes costs one HTTP read for its rows; batches that queue together share one. A new stop's venue and limit price are pending until the SDK reads them, as a snapshot does, and a failed read is tried again up to three times. The view keeps its last data as stale while it recovers, and never sends indexerHeaders. With a chain socket, the stops also follow the stop registries' events, ahead of the indexer: one eth_subscribe("logs") over every registry the main-indexer market list names (one request per market type at most once a minute, for the newest 1,000 markets of each), one eth_getLogs over at most 1,000 blocks each time it opens, and one block read per block of the account's stop changes. | client.accountOrders.subscribe |
| Chain account view of perp positions: the MarginBank's own list of the account's pools, each position, its pool's price and health, the account's resting orders in those pools, and the account's margin, all at one pinned block, in up to three Multicall3 calls, or one when a previous read named the pools and orders. Each pool's order ids are one more call, sent as the account, because the pool answers for its caller. Position figures are the MarginBank's own math; liquidation prices are solved from the same block. No indexer. | client.accountPositions.getSnapshot |
| Chain account view of perp positions, followed live: the same one-block read, pushed by the node (somnia_watch on the chain socket, one Multicall3 call per push) when the MarginBank emits an event naming the account in topic 1, 2 or 3 (one subscription each), and on every price write of addresses.priceFeedScheduler while the account holds positions. The client reads for the first view, once each time the push opens, after a new pool or order or a failure, and every 5 s when pushes stop. A node that does not push leaves the client reading on the account's events (eth_subscribe("logs")) and every 5 s. A failed read keeps the last data as stale. | client.accountPositions.subscribe |
| Chain wallet balances of several accounts: balanceOf for each token and Multicall3's getEthBalance for the native token, all at one pinned block in one Multicall3 call, or one call per balance without Multicall3. No indexer. | client.accountBalances.getSnapshot |
| Chain wallet balances of several accounts, followed live: account/token sets share work within one owner regardless of order, duplicates, or native/sentinel labels; each caller retains its requested order and labels. The same one-block read is repeated at once when one of the tokens emits a Transfer from or to one of the accounts (two eth_subscribe("logs") on the chain socket, the accounts as sender and as receiver), and every 10 s, because a native balance changes with no event. A failed read keeps the last data as stale. | client.accountBalances.subscribe |
| Chain account view of resting spot orders: every spot pool's own list of the account's order ids, one call per pool sent as the account, then the orders' details in one Multicall3 call (one call per order without Multicall3), all at one pinned block. Orders a previous read found ride the first round. Which pools are spot pools comes from the main indexer's market rows, discovered again on each full refresh; nothing about the account does. Failed or unproven empty discovery throws an IndexerError rather than asserting an empty account. A pool whose orders cannot be read is listed in data.unreadPools with its error, and its orders are unknown; a read that can read no pool fails. | client.accountSpotOrders.getSnapshot |
| Chain account view of resting spot orders, followed live: the same read, of the one pool that emitted it, at once when a spot pool emits an order event that concerns the account. eth_subscribe("logs") on the chain socket: every spot pool's OrderPlaced, whose data names the owner, and the later events of the account's own order ids (a fill, as taker or maker, a cancel, expiry, reduce or amend, including in-place amendments); past 1,000 orders, every order event. Every pool once the node confirms a subscription, and 30 s after the last read of every pool. A pool that cannot be read is read again after a backoff; a read that can read no pool keeps the last data as stale. Failed or unproven empty discovery retains known orders and subscriptions with data.complete: false, even if no pool is unread. Pool-specific updates cannot restore complete coverage until full discovery recovers. | client.accountSpotOrders.subscribe |
| Chain ADL queue of one side of one perp market: the MarginBank's wiring, the engine's ranker and scan cap, the pool's status and every holder's state, all at one pinned block, then the armed ranker's rank() over the holders the engine would consider, at that block. No indexer. | client.perpAdlQueue.getSnapshot |
| Chain ADL queue of one side, followed live: the same one-block read, repeated at once when the MarginBank reports a change that can move the queue (three eth_subscribe("logs") on the chain socket: the bank's position, takeover, settlement and funding events that name the pool, and the events that change any account's balance: deposits, withdrawals, credits, linked funding and close-out payments. Order placement and cancellation are left out: they move equity too, but come several times a second), and every 30 s, for a subscription that stops without an error and for what moves the order with no followed event. A failed read keeps the last data as stale. | client.perpAdlQueue.subscribe |
Watch startup and updates
Engine watches return a handle with stop(). Repeated handles for the same scope share its work. The scope normally remains for about 30 seconds after the last handle stops, so a quick remount can reuse it. After teardown, a new watch hydrates again.
Market watches hydrate a main-indexer snapshot before chain backfill and live updates. watchUser hydrates the account's indexed order/fill history. It opens no subscription of its own. Subsequent account updates come only from market scopes watched by this client; it is not an account-wide chain subscription.
A live account view (client.accountOrders.subscribe) starts loading, reads a guarded snapshot as getSnapshot does, and goes ready when the snapshot is accepted. Then it follows the account's change stream from the snapshot's version. The indexer pushes subscription updates at its own interval, 5 s on the deployments measured, so a change can reach the view that long after the indexer processes its block. The stream replays nothing that a lost connection missed. When the connection breaks, or a batch does not follow the version the view holds, the view goes stale with its last data, and the SDK reads a new snapshot after a backoff. An unavailable or tooLarge view stays under watch: the SDK reads again when the capability or the account changes, when the earliest order expires, and every 5 minutes. Listeners of one account share one session, which remains for 30 seconds after the last listener stops.
Price watches hydrate metadata and recent prices from the separate price-feed HTTP service. Each asset key owns a Hasura socket for its price and tick subscriptions. A resolved watch means its snapshot landed, not that the socket has connected. Check getPriceStatus for state. Rejected subscriptions report error, while synchronous price reads retain stale values. These failures do not change market-watch status.
React hooks
| Hook family | Tier |
|---|---|
useLive*, useWatchMarket, useWatchUser, useLiveStatus, usePoolSeedStatus, useIsTailing |
Live store. The pool-keyed hooks open the market watch while mounted; usePoolSeedStatus(pool) reads one pool's seed state without opening a watch. |
useLivePrice, useLivePriceTicks, useLivePriceFeedInfo, useWatchPrice |
Live price store |
usePortfolio, useMarkets, useCandles, useFundingRateSeries, useMarketFees, useOperators, useMarketCreators, useOracleAdapters |
Indexer |
useLendReserves, useLendAccount |
Chain |
useIndexerQuery runs the supplied callback on mount, owner or dependency changes, and explicit refetch. It does not poll or automatically refresh on live events. The callback determines its source and freshness. The hook passes an abort signal and fences superseded responses; cancellation reaches the request only if the callback forwards that signal to an operation that supports it.
Automatic hooks preserve background acquisition semantics: failures use the configured debug/console warning channel and do not throw during render. This includes useFundingRateSeries; its state.error still contains history query failures. Opt into useWatchMarketResult, useWatchUserResult, or useWatchPriceResult for a typed acquisition error, which a component may explicitly throw into an ErrorBoundary. Missing-provider validation still throws InvalidInputError.
SomniaMarketsProvider accepts chainSeedPools, a fixed list of { chainId, pool }
for binary pools that must cold-start from chain. Every pool-watching React hook
shares this choice, so effect order cannot choose the indexer seed first. The
provider reads the current market binding via chain before opening a selected
watch; the selected client must configure addresses.binaryModule. Remount the
provider to change the list. Other pools use the normal indexer seed. The
pool-specific usePoolSeedStatus(pool, { chainId? }) returns null until that
pool has seeded, then exposes seedSource, seedBlock, seedTruncated, and
fillsLoaded. A chain-seeded book can be available with fillsLoaded: false;
indexed history and market discovery still require the indexer.
Response-associated indexer observations
client.createObservedReads() observes one independent RPC head. Its read and batch methods accept the operations in ObservedReadOperation. The roster includes every ordinary main-indexer domain read, including mixed-tier derived reads. It leaves out getUserFillHistory: its result keeps rows from earlier calls, so no response watermark of one call describes it. getSyncStatus, waitForIndexedBlock and getIndexerFreshness are source diagnostics, not row reads. Live-store reads, chain-only reads, write capabilities and the separate price service are excluded.
The envelope keeps the domain value in data. observation.latestProcessedBlock is the minimum metadata watermark across successful contributing responses. Missing metadata in any response leaves it null. startedAt, completedAt and responseCount describe the observation span. The metadata is selected beside the rows in the same GraphQL request, without a separate metadata request. A rejected privileged aggregate attempt does not contribute; its successful fallback does. Observed portfolio reads refresh their scope inside the logical read so a cached pool set cannot hide an older contributing response.
These are indexing watermarks. They do not claim each entity changed at that block, that the RPC itself is current, or that separate requests share an atomic historical snapshot. Keep an envelope with its value; later reads cannot update its observation. Create a new observed capability to refresh the shared head. Closing the owner cancels pending observations; a stopped capability cannot reopen transports.
fetchStatus() remains a local-only three-field chain-tail snapshot. Opt into fetchDataStatus() to read RPC and the main indexer once. Its top-level status retains chain-tail meaning; indexer contains independent measurements and prices lists precise per-asset health. API-only monitors keep using client.getSyncStatus(chainId) with no RPC dependency. client.waitForIndexedBlock(chainId, block) polls the same metadata until the indexer has processed a block, so a caller can read its own write instead of guessing a delay.
The concrete exchange.client exposes the additive SomniaMarketsClientWithObservations contract. SomniaMarketsClient includes waitForIndexedBlock, and order-list selections include userData. Custom SomniaMarketsClient implementations must provide the wait, and a serving indexer must expose Order.userData. Other ordinary reads retain their document selections and role permissions. Only observed reads select the response watermark. stopLive() invalidates observed capabilities and cancels pending indexed-block waits without aborting ordinary pending indexer reads; a later wait relinks to the replacement owner signal. Caller cancellation still reaches those reads.
Consumer example
Use the configured exchange in dreamDEX or another consumer. The downstream dreamDEX hook edit remains outside this repository.
const observed = await exchange.client.createObservedReads();
const [markets, count] = await observed.batch([
{ operation: "listMarkets", args: [{ limit: 20 }] },
{ operation: "countMarkets", args: [] },
]);
console.log(markets.data, markets.observation.latestProcessedBlock);
console.log(count.data, count.observation.independentHead.blockNumber);
const freshness = await exchange.client.getIndexerFreshness(exchange.client.config.chain.id);
console.log(freshness?.lagBlocks, freshness?.lagSeconds);
For a local end-to-end observation, start ./demo-clob.sh up, construct the exchange with the local chain and deployment configuration used by the explorer, and run this example against http://127.0.0.1:8085/v1/graphql and ws://127.0.0.1:8545. Use chain id 31337. No production configuration or transaction is required.
What still works when the main indexer is down
This section tells you which SDK capabilities survive a main-indexer outage. Use it to plan degraded operation. It groups capabilities by what they need, not by method name. The tier tables above remain the per-method reference.
Source-derived at ebe97c4874b5394a3f9bc8cb2ccd9f46b1b1bda8; no outage was induced.
The scope is the main markets indexer. The price-feed service is a separate endpoint with its own configuration and its own failures, so a main-indexer outage does not stop fetchPrice, fetchPriceOHLCV, or the price watches. A chain RPC endpoint must still be reachable for everything in "Works on chain".
Works on chain
These reads need chain RPC only. They do not send a main-indexer request.
One precondition applies to every UNIFIED verb below, the ones named fetch*. They take a symbol, and resolving a symbol reads the in-memory registry that loadMarkets fills from the indexer. So they answer during an outage only for markets loaded BEFORE it began; on a cold start they throw InvalidInputError for an unknown symbol. The Engine reads, which take a pool or market address, have no such precondition.
| Capability | Methods |
|---|---|
| Order books | getBinaryOrderBook, getSpotOrderBook, getBookTopsOnchain; fetchOrderBook too, but only for a symbol the registry already loaded |
| Own orders on chain | getOwnOpenOrdersOnchain (ids), getOpenOrdersOnchain (full rows, several pools, one block), getOrderOnchain (one order), getAllOpenOrdersOnchain |
| Perp positions and state | getPerpPosition, getPerpState, getMarginAccount, getAccountHealth, getAccountEquity, listAccountEquities, getLiquidationPrice |
| Balances | getErc20Balance, getNativeBalance, getBalances, getOutcomeBalance, getVaultBalance |
| Markets and system by id | getMarketOnchain, getSystemInfo, getHeadBlock, getContractMeta, getPoolCreator |
| Pool to market binding | getPoolMarketBinding, getMarketIdOnchain |
| Settlement and provenance | getSettlement, poolCreator |
| Cancel every resting order | cancelAllOrders (default source: "chain") |
cancelAllOrders is the kill switch. Its default source reads the caller's resting order ids from the pool contract, so it works with the main indexer down. Two limits apply. The method belongs to Trader and acts on one pool, and the unified facade has no equivalent verb. The caller must therefore already hold the pool address: spot pool discovery by symbol is indexer-only, so a cold start during an outage cannot resolve one.
Works from the live store while a watch is live
A hydrated watch keeps answering from its store, because its updates arrive over chain subscriptions.
getLive*selectors, includinggetLiveUserOrders,getLiveFills, and the live order books.cancelAllOrders({ source: "live" }). This mode throwsNotConfiguredErrorunless the watch covering the pool is live. A watch that is still hydrating or is reconnecting is not live.
A binary watch can also START with the main indexer down. Call watchMarket(pool, { seed: "chain", marketId }), which seeds the book from the pool contract. It covers one pool per call, so N pools need N calls. Read the marketId that call needs with getPoolMarketBinding(pool), which is also chain-only, so the whole cold start needs no main-indexer request. The discover-all watch cannot start, because it has no binding list to seed from.
Unavailable by design
These capabilities are main-indexer reads. They throw IndexerError while it is unreachable. Absence of data is never substituted for the failure.
- All history: candles, fills, fees, funding payments, liquidations, and market activity.
getOpenOrdersand the other indexed order reads.- Portfolios and PnL:
getPortfolio,getSpotPortfolio,getPerpPortfolio,getBinaryPositionPnL,getOpenPositionsWithPnL,getClaimable. - The discover-all watch,
watchMarkets. - Every freshness surface:
getSyncStatus,getIndexerFreshness, andfetchDataStatus.fetchStatus()is local and still answers. - On an exchange with two or more chains,
fetchDataStatusresolves when some chains fail. It reports each failed chain's error inindexerErrorByChain, and it throws only when every chain fails.
One whole-call failure
One unified method fails completely, although part of its result comes from chain. Use the substitute instead.
fetchTicker used to belong here. Its candle read is now contained, so the call resolves with statsState: "unavailable" and still reports the perp chain numbers.
| Method | Why it fails | Substitute |
|---|---|---|
fetchBalance |
getPortfolio runs inside Promise.all with no catch. |
Use getErc20Balance, getNativeBalance, or getBalances for the chain-held balances. |