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

TierSource and freshnessStartup requirementsCost
Market live storeMain-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.
ChainChain 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 indexerEnvio/Hasura GraphQL over HTTP. Values reflect indexed state and can lag the chain.The main indexer endpoint; aggregate reads can require privileged headers.One or more HTTP requests, depending on pagination and composition. No fixed freshness delay is guaranteed.
Price-feed indexer and live price storeSeparate 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 and open one socket per watched asset key. 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 null when the row does not exist and throws IndexerError when the request fails.
  • An indexer list read resolves to [] when there are no rows and throws IndexerError when the request fails.
  • A chain read throws on failure. It never returns null in 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.
  • A chain-seeded watch loads no fill history, so getLiveFills can be empty while the book is complete. getLiveStatus().fillsLoaded separates the two states: false means the history was never loaded, true means there are genuinely no fills in the loaded range. A failed fill read sets failure.operation to "fill-history" and does not empty the book.
  • Orders a chain seed writes carry an empty createdAt and txHash: placement history is indexer-only and the seed does not invent it. userOrders sorts on createdAt, 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, null or zero and STAY that way: a seeded pool is not re-hydrated by a later watch, and the row outlives stop(), so the seeded row persists for the store's lifetime. Read the indexer directly (getBinaryMarket) when those fields are needed. strike/mode are null rather 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. 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() reports seedSource ("indexer", "chain", or null before the first watch), seedBlock, and seedTruncated. 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; seedTruncated is true only when the drain reaches its own page budget.
  • 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 a warn event on the debug channel.

Exchange API (SomniaMarkets)

MethodTier
loadMarkets, fetchMarketsIndexer, 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.
fetchOrderBookChain
fetchTrades, fetchOHLCV, fetchOpenOrders, fetchOrders, fetchMyTrades, fetchOpenStopOrders, fetchPortfolioAnalytics, fetchFundingRateHistoryIndexer
fetchTickerCached market metadata and main-indexer candles; on perps, also best-effort chain state. last can fall back to cached market data, and lastSource is then absent — that absence is the stale signal. 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".
fetchBalanceLoads market/currency metadata through loadMarkets, then reads chain token/native balances and main-indexer binary holdings. Cost depends on currencies and metadata discovery.
fetchFundingRate, fetchPositionsChain
fetchPrice, fetchPriceOHLCVSeparate price-feed Hasura HTTP
market, priceToPrecision, amountToPrecisionLocal
watchOrderBook, watchTrades, watchOrders, watchMyTradesLive store. The first call opens the market watch.
watchPriceLive 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.

Engine (SomniaMarketsClient)

The tables in this section are the canonical Engine inventory. Names refer to exchange.client, 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.

<!-- engine-inventory:start -->

Live store

GroupMethods
Market selectors and statusgetLiveMarkets, getLiveMarketByPool, getLiveMarketByAddress, getLiveFills, getLiveUserFills, getLiveUserOrders, getLiveFundingUpdates, getLiveBinaryOrderBook, getLiveBinaryOrderBookByMarket, getLiveSpotOrderBook, getLiveStatus, getWatchStatus, isTailing, subscribeLive, listLiveLiquidations, listLiveFundingPayments, getPerpPlaneStatus
Price feedgetLivePrice, getLivePrices, getLivePriceTicks, getLivePriceFeedInfo, getPriceStatus, subscribePrices
WatcheswatchMarket, watchMarkets, watchUser, watchPrice, watchPrices, watchPerpMarkets

Chain

GroupMethods
Books and ordersgetBinaryOrderBook, getSpotOrderBook, getOrderOnchain, getBookTopsOnchain, getOwnOpenOrdersOnchain, getAllOpenOrdersOnchain
Markets and systemgetMarketOnchain, getContractMeta, getHeadBlock, getSystemInfo, getTransactionSummary, getPoolCreator, getFreePools, getOnchainResolutionPrice, getMaxVenueFeeBps, encodeBinaryVenueFeeParams
Balances and allowancesgetErc20Balance, getErc20Metadata, getErc20Allowance, getBalances, getNativeBalance, getOutcomeBalance, getVaultBalance, getOwnLockedBalance, getLockedTokenBreakdown
Operators and buildersgetOperatorPermissionsRegistry, isOperatorAuthorized, isGloballyApproved, isApprovedForPool, getMaxBuilderFeeBpsTimes1k, getBuilderApproval, getEffectiveBuilderApproval, getStopOrderSomiPayment, getPerpStopOrderSomiPayment, getPerpStopOrder, getPerpStopOrders, getUnclaimedPerpStopSomi, getManualVaultMode, getAutoPullRequirement, convertToQuoteAtPriceCeil
PerpsgetPerpState, getPerpFeedStatus, getPerpPosition, getMarginAccount, getAccountHealth, 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, getPerpFundingPremium, meetsPerpImForFill, quoteMeetsPerpImForOrder, quotePerpOrderTopUp
Oracle hubgetSchedulingCost, earmarkedOf, creditOf, outstandingOf, withdrawableOf, payerCreditOf, payerOf, resolveReserve, quoteCreateMarketValue
SomniaLendclient.lend.listReserves, client.lend.getAccount
Smart accountsclient.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

GroupMethods
MarketslistMarkets, listRegistryMarkets, listRegistryMarketsChecked, countMarkets, countMarketsBounded, getMarket, getMarketByPool, listMarketsByPool, listBinaryMarkets, listLiveBinaryMarkets, listPastBinaryMarkets, getBinaryMarket, getBinaryMarketByAddress, listBinaryVenueIds, listBinaryAssets, countBinaryMarkets, countBinaryMarketsBounded, listSpotMarkets, getSpotMarket, listPerpMarkets, getPerpMarket, getMarketFees, getMarketStatusHistory
HistorygetCandles, getFills, getFill, getUserFills, countUserFills, getOrderFills, getOrder, getOrders, getOpenOrders, countOrders, getBookTops, listSweepableOrders, getMarketActivity, getTradeContext, getRouterActions, getUserFillsPage, getFillsPage, getLatestFillBlock, getLatestActiveBlock
PortfoliosgetPortfolio, getSpotPortfolio, getPerpPortfolio, getOutcomeBalances, getSpotStopOrders, listPerpStopOrders, listPerpOrderHistory, listPerpPositions
ResolutiongetMarketResolution, getOpeningPrices, getResolutionPrices
Fees and perps historylistProtocolFees, listBuilderFees, listSettlementFees, listBuilderApprovals, getVaultPayoutFallbacks, getFundingPayments, getMarginEvents, listLiquidations, getLiquidations, listFundingRateHistory, listFundingRateCandles, listPerpFees, getOpenInterestHistory, getFundingRateHistory, listPerpOrderRejections, listPerpInsuranceFundEvents, listPerpWalletLinkEvents, listPerpMarginPulls, listPerpMainFundingEvents
Control planelistOperators, getOperator, countOperators, listVenues, getVenue, countVenues, listMarketCreators, getMarketCreator, listOracleAdapters, getOracleAdapter, listSeries, listSeriesLatestMarkets, getSeries, getOracleQuestion, listOracleQuestions, getOperatorHubAccount, listOperatorHubAccounts, listOracleBinds, listOracleCallbacks, getSyncStatus, waitForIndexedBlock, getPool, getPoolBindings

Price-feed HTTP reads

GroupMethods
Current prices and feed metadatafetchPrice, fetchPrices, fetchPriceFeedInfo, listPriceFeeds
Price historyfetchPriceHistory, fetchPriceCandles

Mixed and derived reads

Inputs and behaviorMethods
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. Without a caller-supplied anchor, tries a chain receipt and block lookup first. Failure to obtain that anchor leaves the indexer-only path 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, router actions, and indexed book tops. All inputs can lag; this does not use the live book or a chain book.getOpenPositionsWithPnL
Main-indexer positions and resolution data, plus indexed settlement fees for winning markets. This estimates claimable amounts from indexed state.getClaimable

| Independent RPC head, then main-indexer metadata plus processed-block time. Raw measurements, no SDK stale threshold. | getIndexerFreshness | | One independent head for response-associated reads, using the same owner. | createObservedReads | | Per-asset price watch lifecycle, without I/O. | getPriceHealth, listPriceHealth |

<!-- engine-inventory:end -->

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.

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 familyTier
useLive*, useWatchMarket, useWatchUser, useLiveStatus, useIsTailingLive store. The pool-keyed hooks open the market watch while mounted.
useLivePrice, useLivePriceTicks, useLivePriceFeedInfo, useWatchPriceLive price store
usePortfolio, useMarkets, useCandles, useFundingRateSeries, useMarketFees, useOperators, useMarketCreators, useOracleAdaptersIndexer
useLendReserves, useLendAccountChain

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.

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. 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.

ts
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.