@somnia-chain/markets-sdk


@somnia-chain/markets-sdk / index / SomniaMarkets

Class: SomniaMarkets

Defined in: packages/sdk/src/unified/exchange.ts:667

Constructors

Constructor

new SomniaMarkets(config): SomniaMarkets

Defined in: packages/sdk/src/unified/exchange.ts:816

Create one owner for Markets reads, watches, and optional signing. Reconciliation is off unless its positive block interval and bounded depth are supplied.

Parameters

config

SomniaMarketsConfig

Returns

SomniaMarkets

Throws

InvalidInputError If reconciliation bounds are invalid, or if indexerLimits carries a bound the governor cannot use — a count that is not a whole number of at least 1, a delay of 0 or less, or any non-finite value. Checked here so a limit that would disable the in-flight cap or the backoff schedule fails at construction rather than under load. Also if chains is empty, binds one chain twice, or binds one indexer to two chains (URLs compare with scheme and host lower-cased, default port dropped and trailing slashes dropped).

Throws

NotConfiguredError If the indexer endpoint is absent.

Properties

markets

markets: Record<string, UnifiedMarket> = {}

Defined in: packages/sdk/src/unified/exchange.ts:673

Unified markets keyed by MARKET symbol (populated by loadMarkets).


symbols

symbols: string[] = []

Defined in: packages/sdk/src/unified/exchange.ts:675

All market symbols (populated by loadMarkets).


has

readonly has: object

Defined in: packages/sdk/src/unified/exchange.ts:692

Capability map — which unified verbs this venue supports (the ccxt exchange.has convention, for capability-probing bot code). Every listed verb is implemented here, so every flag is true.

fetchMarkets

readonly fetchMarkets: true = true

SomniaMarkets.fetchMarkets

fetchOrderBook

readonly fetchOrderBook: true = true

SomniaMarkets.fetchOrderBook

fetchTrades

readonly fetchTrades: true = true

SomniaMarkets.fetchTrades

fetchOHLCV

readonly fetchOHLCV: true = true

SomniaMarkets.fetchOHLCV

fetchBalance

readonly fetchBalance: true = true

SomniaMarkets.fetchBalance

fetchOpenOrders

readonly fetchOpenOrders: true = true

SomniaMarkets.fetchOpenOrders

getOrderHistoryPage

readonly getOrderHistoryPage: true = true

SomniaMarkets.getOrderHistoryPage

getOrdersPage

readonly getOrdersPage: true = true

SomniaMarkets.getOrdersPage

fetchMyTrades

readonly fetchMyTrades: true = true

SomniaMarkets.fetchMyTrades

fetchStatus

readonly fetchStatus: true = true

SomniaMarkets.fetchStatus

createOrder

readonly createOrder: true = true

SomniaMarkets.createOrder

cancelOrder

readonly cancelOrder: true = true

SomniaMarkets.cancelOrder

watchOrderBook

readonly watchOrderBook: true = true

SomniaMarkets.watchOrderBook

watchTrades

readonly watchTrades: true = true

SomniaMarkets.watchTrades

watchOrders

readonly watchOrders: true = true

SomniaMarkets.watchOrders

watchMyTrades

readonly watchMyTrades: true = true

SomniaMarkets.watchMyTrades

fetchPositions

readonly fetchPositions: true = true

SomniaMarkets.fetchPositions

fetchFundingRate

readonly fetchFundingRate: true = true

SomniaMarkets.fetchFundingRate

fetchFundingRateHistory

readonly fetchFundingRateHistory: true = true

SomniaMarkets.fetchFundingRateHistory — the key did not previously exist in this map, so it had to be ADDED rather than flipped.

watchPrice

readonly watchPrice: true = true

SomniaMarkets.watchPrice

fetchPrice

readonly fetchPrice: true = true

SomniaMarkets.fetchPrice

fetchPriceOHLCV

readonly fetchPriceOHLCV: true = true

SomniaMarkets.fetchPriceOHLCV

Accessors

chainIds

Get Signature

get chainIds(): number[]

Defined in: packages/sdk/src/unified/exchange.ts:880

Numeric chain ids this exchange spans, in binding order (the first is the primary).

Returns

number[]


chainIdsWithoutIndexer

Get Signature

get chainIdsWithoutIndexer(): number[]

Defined in: packages/sdk/src/unified/exchange.ts:905

The chains bound with no indexerUrl, in binding order. This is how the account reads report which chains they skipped.

A chain-only binding, such as Hideki (50383) with no hosted indexer, can read balances and resting orders from the chain but has no indexed data. On a read over more than one chain, the facade account reads skip the indexer legs of these chains instead of throwing NotConfiguredError:

A skipped chain adds no rows. Its absence from a result does not mean that the account has no orders or trades there. A read scoped to one of these chains still throws NotConfiguredError when it has no chain-readable leg.

Returns

number[]


client

Get Signature

get client(): SomniaMarketsClientWithObservations

Defined in: packages/sdk/src/unified/exchange.ts:924

The native engine of a single-chain exchange — bigint-exact, address-keyed. The escape hatch for reads and watches the unified verbs do not cover.

Gotchas

On an exchange with more than one chain binding this throws. A pool address or market id can name a different market on each chain, so a read with no chain could return another chain's data. Use SomniaMarkets.engineFor with the chain id there.

Throws

InvalidInputError If the exchange spans more than one chain. chainIds lists the chains it spans.

Returns

SomniaMarketsClientWithObservations


trader

Get Signature

get trader(): Trader

Defined in: packages/sdk/src/unified/exchange.ts:1092

The raw write tier of a single-chain exchange, bound to its signer — bigint-exact placeOrder/mintSet/faucet/… for anything the unified verbs don't cover.

Gotchas

On an exchange with more than one chain binding this throws. A pool address or market id can name a different market on each chain, so a write with no chain could land on another chain. Use SomniaMarkets.traderFor with the chain id there.

Throws

InvalidInputError If the exchange spans more than one chain (chainIds lists them), or the private key is malformed.

Throws

SignerRequiredError If no signer is configured.

Throws

NotConfiguredError If the account has no executor for the chain.

Returns

Trader


walletAddress

Get Signature

get walletAddress(): `0x${string}` | undefined

Defined in: packages/sdk/src/unified/exchange.ts:1121

The authenticated wallet address, if a signer was configured.

Returns

`0x${string}` | undefined


perpDiscoveryError

Get Signature

get perpDiscoveryError(): SomniaMarketsError | null

Defined in: packages/sdk/src/unified/exchange.ts:1187

Why chain-tier perp discovery did not run on the last loadMarkets, or null when it ran (or was never applicable).

loadMarkets() contains a discovery failure rather than throwing, because it is the implicit prerequisite of nearly every symbol-based verb and a chain failure must not take the SPOT and OUTCOME market lists down with it. This is where that contained failure is reported, with the underlying error preserved in cause.

When to use

Check it after loadMarkets() whenever a SHORT perp list would be worse than an error — a market page, an order router, anything that would otherwise present "this market does not exist". Then either tell the user the list is incomplete, or call loadMarkets() again. A load that recorded a failure is not served from the cache, so the next bare call re-runs discovery. loadMarkets(true) also retries.

Gotchas

  • On a multi-chain exchange this is the FIRST failing chain's error, in the order of the chains config. perpDiscoveryErrorsByChain reports every failing chain.
  • Do NOT infer this from perpStatus being absent. That works only when the indexer already carries a perp row to inspect; with a configured factory and an indexer carrying none, a failed discovery yields an EMPTY perp list with no market to check. This accessor answers in both cases.
  • null does not mean the venue has perps. A chain with no perps plane deployed (local anvil) never attempts discovery and reports null too. perpDiscoverySkipped reports that case, so read BOTH before you tell a user the venue has no perps.

Example (Refusing to show a possibly-short perp list)

ts
await exchange.loadMarkets();
if (exchange.perpDiscoveryError) {
  // loadMarkets(true) reloads every chain.
  await exchange.loadMarkets(true);
}
if (exchange.perpDiscoveryError) {
  const partial = exchange.perpDiscoveryPartialFailure;
  throw new Error(
    partial
      ? `perp list incomplete: ${partial.failed} of ${partial.total} markets unread`
      : "perp discovery failed; the perp list may be short",
  );
}
Returns

SomniaMarketsError | null


perpDiscoveryErrorsByChain

Get Signature

get perpDiscoveryErrorsByChain(): Record<number, SomniaMarketsError>

Defined in: packages/sdk/src/unified/exchange.ts:1213

Every chain whose perp discovery failed on the last loadMarkets, keyed by chain id. A chain with no failure has no key, so {} means discovery failed on no chain.

perpDiscoveryError reports only the first failing chain. On a multi-chain exchange, use this accessor to name each chain whose perp list may be short. Each value is the same typed error that perpDiscoveryError would hold for that chain alone, with the underlying error in cause.

Gotchas

  • Each read returns a new object. Changes to it do not change the exchange.
  • It resets on every load that runs, so it always describes the LAST load.
Returns

Record<number, SomniaMarketsError>


perpDiscoveryPartialFailure

Get Signature

get perpDiscoveryPartialFailure(): { failed: number; total: number; } | null

Defined in: packages/sdk/src/unified/exchange.ts:1226

How much of the chain-only perp set was lost when discovery PARTIALLY failed, or null when it did not.

Typed counts rather than prose in a message, so a consumer can decide on them — { failed: 1, total: 4 } reads as "three of four chain-only markets are listed". Always null when perpDiscoveryError is null, and also null when discovery failed outright rather than partly (nothing was read, so there is no ratio).

Returns

{ failed: number; total: number; } | null


perpDiscoveryPartialFailuresByChain

Get Signature

get perpDiscoveryPartialFailuresByChain(): Record<number, { failed: number; total: number; }>

Defined in: packages/sdk/src/unified/exchange.ts:1245

The partial-discovery counts of the last loadMarkets, keyed by chain id. A chain has a key only when its discovery PARTIALLY failed.

perpDiscoveryPartialFailure reports the counts of the first failing chain only. This accessor gives the counts of each chain, so { 50312: { failed: 1, total: 4 } } reads as "chain 50312 lists three of its four chain-only markets". A chain whose discovery failed outright has a key in perpDiscoveryErrorsByChain and no key here, because nothing was read.

Gotchas

  • Each read returns a new object. Changes to it do not change the exchange.
Returns

Record<number, { failed: number; total: number; }>


perpDiscoverySkipped

Get Signature

get perpDiscoverySkipped(): { reason: "no-factory-route"; } | null

Defined in: packages/sdk/src/unified/exchange.ts:1300

Why the last loadMarkets did not LOOK for perp pools, or null when it did look.

"no-factory-route" means the SDK had no way to reach the PerpPoolFactory. It finds the factory either from a configured addresses.perpPoolFactory, or from the marginBank of an indexed perp row. With neither — a reachable indexer that carries no perp rows, and no configured address — discovery returns an empty set without asking anything.

This is the one state the error accessors cannot report. Nothing failed, so perpDiscoveryError and registryReadError are both null, and an empty perp list would otherwise be indistinguishable from a venue that genuinely has no perps.

When to use

Read it before you tell a user "this venue has no perps". A non-null value means "not looked", not "none exist".

Gotchas

  • This is INFORMATIONAL, not an alert. A non-null value is normal and permanent on a venue with no perps plane — Somnia mainnet today, and a local anvil deployment. Do not surface it as an error there.
  • It reports "the SDK had no way to look", never "a read failed". A failed registry read on a chain leaves this null for that chain: the empty row set is then a symptom of that failure, which registryReadError already names.
  • For ONE engine it is likewise never set together with perpDiscoveryError. That accessor means a route was found and the read failed; this one means no route was found and nothing was read. On a multi-chain exchange both can be non-null at once, each reporting a different chain.
  • It resets on every load, so re-read it after loadMarkets(true).
  • The realistic transient cause is an indexer that was wiped or is reindexing below the factory's first deploy block. The spot and binary rows are short in that window too; registryReadError does not cover it, and exchange.client.getIndexerFreshness is the general signal for lag.

Example (Telling "no perps here" apart from "could not look")

ts
await exchange.loadMarkets();
const perps = Object.values(exchange.markets).filter((m) => m.type === "swap");
if (perps.length === 0 && exchange.perpDiscoverySkipped) {
  console.log("perp discovery was skipped:", exchange.perpDiscoverySkipped.reason);
}
Returns

{ reason: "no-factory-route"; } | null


registryReadError

Get Signature

get registryReadError(): SomniaMarketsError | null

Defined in: packages/sdk/src/unified/exchange.ts:1346

Why the indexer's market registry could not be read on the last loadMarkets, or null when the read COMPLETED.

loadMarkets() contains this failure rather than throwing, for the same reason it contains a perp discovery failure: it is the implicit prerequisite of nearly every symbol-based verb, and an indexer outage must not take down the perp markets the chain can enumerate on its own. This is where that contained failure is reported, with the underlying error preserved in cause. In practice it is an IndexerError carrying operation and detail.

When to use

Check it after loadMarkets() whenever an EMPTY market list would be worse than an error. null together with an empty symbols is the unambiguous "this venue genuinely has no markets"; non-null with an empty list means the registry could not be read and the list says nothing about what exists. To retry, call loadMarkets() again. A load that recorded a failure is not served from the cache, so the next bare call re-runs the sweep. loadMarkets(true) also retries.

Gotchas

  • On a multi-chain exchange this is the FIRST failing chain's error, in the order of the chains config. registryReadErrorsByChain reports every failing chain.
  • This is all-or-nothing for one chain. A registry sweep that succeeded but dropped unparseable rows is a different condition, reported by listRegistryMarketsChecked through RegistrySweep.dropped.
  • Cleared on every loadMarkets() that actually runs, so it always describes the LAST load, never an older one.

Example (Telling an outage apart from an empty venue)

ts
await exchange.loadMarkets();
if (exchange.registryReadError) {
  // loadMarkets(true) reloads every chain.
  await exchange.loadMarkets(true);
}
if (exchange.registryReadError) {
  throw new Error("the market registry could not be read; the market list may be short");
}
if (exchange.symbols.length === 0) {
  console.log("this venue has no markets");
}
Returns

SomniaMarketsError | null


registryReadErrorsByChain

Get Signature

get registryReadErrorsByChain(): Record<number, SomniaMarketsError>

Defined in: packages/sdk/src/unified/exchange.ts:1368

Every chain whose indexer registry read failed on the last loadMarkets, keyed by chain id. A chain with no failure has no key, so {} means every registry read completed.

registryReadError reports only the first failing chain. On a multi-chain exchange, use this accessor to name each chain whose market list may be short. Each value is the same typed error that registryReadError would hold for that chain alone. In practice it is an IndexerError carrying operation and detail.

Gotchas

  • A chain bound with no indexerUrl always has a key here. Its value is a NotConfiguredError, because that chain has no registry to read.
  • Each read returns a new object. Changes to it do not change the exchange.
  • It resets on every load that runs, so it always describes the LAST load.
Returns

Record<number, SomniaMarketsError>


accounts

Get Signature

get accounts(): ExchangeSmartAccounts

Defined in: packages/sdk/src/unified/exchange.ts:3985

The smart-accounts surface across this exchange's chains: derive the one address, scan every covered chain, fan one signed authorization out into per-chain creation calls, and build the multi-chain account the exchange trades through. See ExchangeSmartAccounts; the one-chain surface is client.accounts.

Returns

ExchangeSmartAccounts


collateralReadErrors

Get Signature

get collateralReadErrors(): Readonly<Record<number, CollateralReadError>>

Defined in: packages/sdk/src/unified/exchange.ts:4012

Failed reads from the last completed collateral balance sweep, keyed by chain. Available after createOrder checks or ensureFunds, including successful bridge plans. Empty before the first sweep and after a complete successful read. Target-chain failures throw and leave this snapshot unchanged. Concurrent sweeps replace it in completion order; a shortfall error retains its own snapshot.

Returns

Readonly<Record<number, CollateralReadError>>


bridge

Get Signature

get bridge(): ExchangeBridge

Defined in: packages/sdk/src/unified/exchange.ts:4021

The bridge between this exchange's chains, through the account's executors: plan a transfer, or execute one and wait for it to land. See ExchangeBridge.

Returns

ExchangeBridge

Methods

engineFor()

engineFor(chainId): SomniaMarketsClientWithObservations

Defined in: packages/sdk/src/unified/exchange.ts:949

The native engine bound to one configured chain — bigint-exact, address-keyed. The escape hatch for that chain.

Gotchas

Throws NotConfiguredError for a chain this exchange has no binding for.

Parameters

chainId

number

Returns

SomniaMarketsClientWithObservations


traderFor()

traderFor(chainId): Trader

Defined in: packages/sdk/src/unified/exchange.ts:1046

The raw write tier for one chain this exchange spans, bound to its signer — bigint-exact placeOrder/approveBuilder/setPerpLeverage/… for anything the unified verbs do not cover.

Details

The trader uses the engine of chainId, so its writes share that engine's nonce lane. With a SomniaMarketsAccount it writes through account.executors[chainId]. It is built on first use and replaced after SomniaMarkets.setSigner or SomniaMarkets.close.

Parameters

chainId

number

Returns

Trader

Throws

NotConfiguredError If the exchange has no binding for chainId, or the account has no executor for it.

Throws

SignerRequiredError If no signer is configured.

Throws

InvalidInputError If the private key is malformed or the signer cannot send on chainId.


setSigner()

setSigner(signer): void

Defined in: packages/sdk/src/unified/exchange.ts:1109

Bind (or replace) the exchange's signer after construction. Browser apps construct the exchange at boot for public reads, then call this when the user's wallet connects — and again with {} on disconnect, which returns the exchange to unauthenticated reads. Replaces the trader every authenticated verb and walletAddress resolve against; live watches and market data are unaffected.

Parameters

signer

SomniaMarketsSignerConfig

Returns

void

Throws

InvalidInputError If a SomniaMarketsAccount has an executor for a chain this exchange does not span, for another chain, or acting as another address. The previous signer stays bound.


loadMarkets()

loadMarkets(reload?): Promise<Record<string, UnifiedMarket>>

Defined in: packages/sdk/src/unified/exchange.ts:1420

Load (or reload) the market registry: every market as a unified, symbol-keyed market object. Call once before anything symbol-based.

Perp markets come from two sources. The indexer's rows are unioned with the PerpPoolFactory's, because the indexer builds its perp set dynamically from PerpPoolDeployed and a pool deployed above its current head is live on chain and absent there. A market only the chain knows carries UnifiedMarket.indexed false; read that field's docs before touching anything history-derived on it.

A read failure is contained, not thrown. This method is the implicit prerequisite of nearly every symbol-based verb, so letting one source's failure out would take every other source down with it. Both sources report instead of throwing: the indexer registry read through registryReadError, and perp discovery through perpDiscoveryError — which also covers the PARTIAL case where some chain-only markets were read and others were not. Each chain loads on its own, and registryReadErrorsByChain, perpDiscoveryErrorsByChain and perpDiscoveryPartialFailuresByChain report the failures of each chain. So an indexer outage still returns the perp markets the chain can enumerate, and a chain outage still returns the indexer's spot and outcome lists. A caller that treated a throw as "the registry is unreadable" must now check registryReadError instead.

Discovery coverage per kind.

  • Perp — chain-complete through the PerpPoolFactory, so it survives an empty or unreachable indexer. One caveat: with NO indexed perp row the factory is found only from a configured addresses.perpPoolFactory, because the bootstrap route otherwise reads the marginBank off an indexed row. Configure it for a venue whose indexer carries no perp rows.
  • Binary — indexer-only here. Live discovery of NEW binary markets does not need this method: watchMarkets({ discover: true }) adds them from the chain's MarketCreated events.
  • Spot — indexer-only. SpotPoolRegistry answers "is this pool registered", so it cannot be enumerated from the chain.

Gotchas

  • A bare call returns the cached registry when every chain is loaded and the last load recorded no failure. When a chain's registry read or perp discovery failed, a bare call reads that chain again, so a chain that failed once does not stay missing. A chain that no load has read yet, for example after only reads filtered to another chain, loads the same way. The other chains keep their cached markets. loadMarkets(true) reloads every chain.
  • A verb that loads markets for you, such as fetchBalance, fetchMyTrades or fetchPositions, retries a failed chain at most once in 30 seconds. The 30 seconds start when that chain's last load settled. Before then, the verb uses the cached markets, and the failure accessors keep the recorded error and failing chain. So a persistent failure does not re-run the full registry sweep and perp discovery on every verb. A bare loadMarkets(), fetchMarkets(), ensureFunds and loadMarkets(true) do not wait: they retry at once.
  • A call that starts while a retry of a chain it needs is running waits for that retry and gets its result. It does not start a second retry.
  • Symbols are unique across the loaded chains. When a chain loads later than the others, a symbol that it shares with a market on another chain changes on both chains.
  • A NotConfiguredError failure does not cause a retry. It reports a missing config value, which a retry cannot fix. A chain bound with no indexerUrl records one on every load.
  • Concurrent cold reads share discovery. Each explicit reload waits for prior discovery, then re-reads independently.

Details

  • reload: Re-read everything, including the live tradeability gates and the binary pools' grids.
  • Throws NotConfiguredError - the load needed a chain read (ERC-20 metadata for an unlabeled token, or a binary pool's grid) and the owner has neither a wsRpcUrl nor a chain whose rpcUrls carry a WebSocket endpoint. Thrown before any request goes out, so it means misconfiguration rather than an outage — an outage reaches registryReadError or perpDiscoveryError instead.
  • Throws InvariantError - the SDK's own reasoning was wrong (a market the symbol registry did not name). Report it as a defect; there is nothing for a caller to do about it.

The complete union is SomniaMarketsLoadMarketsError. Neither an unreachable indexer nor an unreadable chain is in it: both are contained and reported through the accessors named above.

Parameters

reload?

boolean = false

Returns

Promise<Record<string, UnifiedMarket>>


market()

market(ref): Tradable

Defined in: packages/sdk/src/unified/exchange.ts:1995

Resolve any handle (symbol, tradable symbol, pool/market address, market id) to its tradable. Requires loadMarkets().

Gotchas

Resolves MORE than fetchMarkets lists. A settled binary market the wallet still holds a position, a fill or an order in is added here the first time a read names it (fetchBalance, fetchMyTrades, fetchOpenOrders, fetchOrders), by market id and BinaryMarket address — never by pool, which by then serves the next market in the series. Such a market can be redeemed and reported; every pool-routed verb refuses it.

Parameters

ref

MarketHandle

Returns

Tradable


priceToPrecision()

priceToPrecision(ref, price): number

Defined in: packages/sdk/src/unified/exchange.ts:2015

Snap a price to the market's tick grid (rounds down; binary prices are also clamped inside (0, 1)).

When to use

Use before createOrder with computed prices.

Spot/perp ticks come from the market row; binary ticks come from the pool, read once by loadMarkets — so a pool recycled mid-session keeps the grid captured at load time until loadMarkets(true) refreshes it.

Gotchas

  • Throws InvalidInputError if the market is binary and its pool's parameters could not be read — quantizing against a guessed grid is what produced off-tick rejections, so this fails loudly instead.

Parameters

ref

MarketHandle

price

number

Returns

number


amountToPrecision()

amountToPrecision(ref, amount): number

Defined in: packages/sdk/src/unified/exchange.ts:2036

Snap an amount to the market's lot grid (rounds down).

Spot/perp lots come from the market row; binary lots come from the pool, read once by loadMarkets — so a pool recycled mid-session keeps the grid captured at load time until loadMarkets(true) refreshes it.

Gotchas

  • Throws InvalidInputError if the market is binary and its pool's parameters could not be read. Previously such a market fell back to a one-whole-token lot, silently flooring every sub-token amount to 0.

Parameters

ref

MarketHandle

amount

number

Returns

number


fetchMarkets()

fetchMarkets(): Promise<UnifiedMarket[]>

Defined in: packages/sdk/src/unified/exchange.ts:2053

Every market as an array — loadMarkets (called if needed), minus the symbol keying.

When to use

Use as the ccxt-shaped sibling for list-style consumers.

Returns

Promise<UnifiedMarket[]>


fetchOrderBook()

fetchOrderBook(ref, options?): Promise<UnifiedOrderBook>

Defined in: packages/sdk/src/unified/exchange.ts:2194

One-shot book read from the contract (head-fresh; no watch needed).

When to use

Use when one book snapshot is enough. For a continuously-current zero-round-trip book, use watchOrderBook. Both sides use one selected head, or options.blockNumber. Empty books keep that pin. A numeric second argument remains the level limit.

Parameters

ref

MarketHandle

options?

number | FetchOrderBookOptions

Returns

Promise<UnifiedOrderBook>

Throws

InvalidInputError If the symbol, depth, or pin is invalid.

Throws

NotConfiguredError If the owner has no chain endpoint.

Throws

RpcError If the head selection or either read fails.

Throws

ContractRevertError If the contract rejects a read.

Throws

InvalidInputError If the market is a SETTLED binary market (resolved, voided or finalized). Its pool now serves the next market in the series, so only redeem, balances and history remain.


fetchTrades()

fetchTrades(ref, since?, limit?): Promise<UnifiedTrade[]>

Defined in: packages/sdk/src/unified/exchange.ts:2224

Recent public trades with authoritative block and log positions (indexer, newest first).

Parameters

ref

MarketHandle

since?

number

limit?

number = 50

Returns

Promise<UnifiedTrade[]>

Throws

InvalidInputError If the symbol or source position is invalid.

Throws

IndexerError If the indexer read fails.

Throws

NotConfiguredError If the chain's indexer is absent or does not index that chain.

Throws

InvalidInputError If the market is a SETTLED binary market (resolved, voided or finalized). Its pool now serves the next market in the series, so only redeem, balances and history remain.


fetchOHLCV()

fetchOHLCV(ref, timeframe?, since?, limit?): Promise<UnifiedOHLCV[]>

Defined in: packages/sdk/src/unified/exchange.ts:2284

OHLCV candles (indexer), oldest first as [ms,o,h,l,c,vol] rows. Timeframes: 1m 5m 15m 1h 4h 1d.

Example (Reading candles)

The last 24 hourly candles, destructured per row.

ts
const candles = await exchange.fetchOHLCV("SOMI/USDC", "1h", undefined, 24);
for (const [ts, open, high, low, close, volume] of candles) {
  console.log(new Date(ts).toISOString(), open, high, low, close, volume);
}
  • Throws InvalidInputError - the market is a SETTLED binary market (resolved, voided or finalized). Its pool now serves the next market in the series, so only redeem, balances and history remain.

Parameters

ref

MarketHandle

timeframe?

string = "5m"

since?

number

limit?

number = 500

Returns

Promise<UnifiedOHLCV[]>


fetchTicker()

fetchTicker(ref): Promise<UnifiedTicker>

Defined in: packages/sdk/src/unified/exchange.ts:2340

Rolling 24h ticker (indexer): OHLC + base/quote volume folded from the hourly candles, last from the freshest fill. NO-outcome tradables view prices through the 1−p lens like every other read.

Gotchas

The two halves of this response fail independently, and each reports its own state. Read statsState and perpState; do not infer either from a missing field.

The 24h half is contained. A candle request that fails with an IndexerError, and a market with indexed: false (no request is sent), both give statsState: "unavailable", and then open, high, low, change, percentage, statsAsOf, baseVolume and quoteVolume are all absent. The call still resolves, so the perp chain numbers — mark, index, funding and open interest — survive an indexer outage. last and lastSource also survive, from the cached market row.

baseVolume of 0 is a true zero: a successful read over a window with no fills. It is not the same as an absent volume.

Example (Reading a ticker)

Drive a price strip off one call.

ts
const tk = await exchange.fetchTicker("SOMI/USDC");
if (tk.statsState === "ok") console.log(tk.last, tk.percentage, tk.baseVolume);
else console.log(tk.last, "24h statistics unavailable");

Parameters

ref

MarketHandle

Returns

Promise<UnifiedTicker>

Throws

InvalidInputError when ref names no loaded market.

Throws

NotConfiguredError when the market's chain has no binding on this exchange.

  • Throws InvalidInputError - the market is a SETTLED binary market (resolved, voided or finalized). Its pool now serves the next market in the series, so only redeem, balances and history remain.

fetchBalance()

fetchBalance(filter?): Promise<UnifiedBalances>

Defined in: packages/sdk/src/unified/exchange.ts:2515

Wallet balances for every currency the loaded markets use (+ native).

Gotchas

free === total: funds escrowed in resting orders live in the pools, not the wallet, so they simply don't appear here.

  • Throws SignerRequiredError - balances are per-account, so this needs a signer (or an account) even though it only reads.
  • Throws NotConfiguredError - filter.chainId names a chain this exchange does not span.
  • Throws IndexerError - the outcome-holdings read needed the indexer and it was unreachable, or the indexer served a position on a market it has no row for. Distinct from an empty result: no balances is {}, not a throw. The loadMarkets() this calls first does NOT throw on an unreachable indexer; it reports through registryReadError, so a balance set can be short rather than absent.
  • Throws RpcError - a chain balance read did not complete. A failed read is never reported as a zero balance or a missing key.
  • Throws ContractRevertError - a token's balanceOf reverted.

With no filter.chainId this reads every chain and throws when any chain fails. On an exchange with more than one chain, the thrown IndexerError or RpcError then has chainId set to the chain that failed. Pass filter.chainId to read one chain only (see SomniaMarketsChainFilter).

A chain bound with no indexerUrl reports its ERC-20 and native balances and no outcome holdings, because holdings are indexer data. Such chains are listed in chainIdsWithoutIndexer.

Example (Reading balances)

ERC-20s key by currency code; binary outcome holdings key by TRADABLE symbol.

ts
const bal = await exchange.fetchBalance();
console.log(bal.USDC?.total);                              // collateral in the wallet
console.log(bal["BTC-95000-31DEC26/USDC#YES"]?.total);     // YES shares held
// A holding on a market that already SETTLED is listed under that market's
// own symbol, even though `fetchMarkets()` no longer lists the market.
for (const [symbol, held] of Object.entries(bal)) {
  if (symbol.includes("#") && !exchange.symbols.includes(symbol.split("#")[0] ?? "")) {
    console.log("settled, redeemable:", symbol, held.total);
  }
}

Parameters

filter?

SomniaMarketsChainFilter = {}

Returns

Promise<UnifiedBalances>


fetchOpenOrders()

fetchOpenOrders(ref?, limit?, filter?): Promise<UnifiedOrder[]>

Defined in: packages/sdk/src/unified/exchange.ts:2644

Open orders (indexer view).

When to use

Use for an occasional snapshot; a trading loop should prefer watchOrders.

Details

limit is applied BY THE QUERY, per venue — not to the merged result. An unscoped call reads all three venues, so it can return up to 3 × limit rows; a ref-scoped call reads only that venue. The default is 200 per venue.

  • ref: Restrict to one tradable (symbol or address). Omit for all.
  • limit: Max orders PER VENUE the query returns (default 200).
  • filter.chainId: Read only this chain.

With no ref and no filter.chainId this reads every chain and throws when any chain fails. On an exchange with more than one chain, the thrown IndexerError, or the RpcError of an on-chain order read, then has chainId set to the chain that failed (see SomniaMarketsChainFilter).

On each indexed chain, the binary, spot and perp portfolio reads run at the same time. The result keeps chain binding order, then binary, spot, perp order within a chain, whatever order the reads finish in. When more than one read fails, the error is the first one to arrive.

On a chain bound with no indexerUrl (see chainIdsWithoutIndexer) this reads the resting orders from the chain instead, over the perp pools that loadMarkets discovered there. Those rows have no txHash and no timestamp.

Gotchas

The indexer view lags the chain slightly.

This read needs the indexer and throws IndexerError while it is unreachable. Do NOT wrap it in a catch that returns an empty list: a bot that does so believes it has nothing resting during an outage, and then quotes again on top of its own live orders. The chain-backed substitute is exchange.client.getOpenOrdersOnchain(pools, owner), which answers from the pool contracts at one pinned block. It returns raw rows without a market, a symbol or a binary YES/NO side, so it is a substitute for quote management rather than a drop-in replacement for this verb.

On SPOT the same limit also bounds the PENDING STOP ORDERS the underlying portfolio read returns — one query variable caps both sets. That list is not part of this verb's result, so the coupling is invisible here, but a caller reading client.getSpotPortfolio directly with a small ordersLimit will see a correspondingly short pendingStopOrders.

Parameters

ref?

MarketHandle

limit?

number

filter?

SomniaMarketsChainFilter = {}

Returns

Promise<UnifiedOrder[]>

Throws

InvalidInputError If ref is unknown, or ref is not on filter.chainId.

Throws

NotConfiguredError If filter.chainId names a chain this exchange does not span.


fetchOrders()

fetchOrders(ref?, since?, limit?, params?): Promise<UnifiedOrder[]>

Defined in: packages/sdk/src/unified/exchange.ts:2740

The wallet's orders across every lifecycle status (indexer), newest first — the history counterpart to fetchOpenOrders. Scope to one tradable with ref; page with limit/params.offset, both forwarded to the query as a true offset window over one ordered set. (Its siblings page differently: fetchMyTrades pages a fill tape to satisfy limit, and fetchOpenOrders applies its limit per venue.)

params.chainId reads only that chain (see SomniaMarketsChainFilter). With no ref and no params.chainId this reads every chain and throws when any chain fails. On an exchange with more than one chain, the thrown IndexerError then has chainId set to the chain that failed. A read over more than one chain skips a chain bound with no indexerUrl (see chainIdsWithoutIndexer).

Example (Reading order history)

The last 50 orders on one book, whatever became of them.

ts
const orders = await exchange.fetchOrders("SOMI/USDC", undefined, 50);
for (const o of orders) console.log(o.status, o.side, o.amount, o.txHash);

Parameters

ref?

MarketHandle

since?

number

limit?

number = 100

params?

SomniaMarketsChainFilter & object = {}

Returns

Promise<UnifiedOrder[]>

Throws

InvalidInputError If ref is unknown, or ref is not on params.chainId.

Throws

NotConfiguredError If params.chainId names a chain this exchange does not span, or the one chain the read touches has no indexer.


getOrderHistoryPage()

getOrderHistoryPage(options?): Promise<UnifiedOrdersPage>

Defined in: packages/sdk/src/unified/exchange.ts:2824

Read one raw-row page of the authenticated owner's indexed order history.

Use this method to traverse indexed order records with immutable order identities. The records and offset window are mutable. They are not an immutable lifecycle log or a current Open Orders snapshot. The default limit is 100 and the default offset is 0. Continuation depends on raw rows, so a full page can have no display orders and still continue. The result is partial because changes can make pages skip or repeat rows. Exhaustion does not prove complete account state.

The status option is an indexed history filter. In particular, status: "Open" cannot establish completeness, removals, continuity, or action freshness. Use a separately supported current-state flow when the caller needs the SDK's current open-order view.

The chainId option reads only that chain. With no ref and no chainId this reads every chain and throws when any chain fails. On an exchange with more than one chain, the thrown IndexerError then has chainId set to the chain that failed. A read over more than one chain skips a chain bound with no indexerUrl (see chainIdsWithoutIndexer).

Example (Following order-history page offsets)

ts
let offset = 0;
while (true) {
  const page = await exchange.getOrderHistoryPage({ limit: 100, offset });
  for (const order of page.orders) console.log(order.identity, order.status);
  if (page.nextOffset === null) break;
  offset = page.nextOffset;
}

Parameters

options?

SomniaMarketsGetOrdersPageOptions = {}

Returns

Promise<UnifiedOrdersPage>

Throws

InvalidInputError If the ref, status, limit, or offset is invalid, or the ref is not on chainId.

Throws

SignerRequiredError If no account is configured.

Throws

IndexerError If the indexer read or producer-row conversion fails.

Throws

NotConfiguredError If chainId names a chain this exchange does not span, or the chain's indexer is absent or does not index that chain.


getOrdersPage()

getOrdersPage(options?): Promise<UnifiedOrdersPage>

Defined in: packages/sdk/src/unified/exchange.ts:2839

Read one raw-row page of the authenticated owner's indexed order history.

This compatibility name has the same result, pagination, chainId option, and errors as SomniaMarkets.getOrderHistoryPage.

Parameters

options?

SomniaMarketsGetOrdersPageOptions = {}

Returns

Promise<UnifiedOrdersPage>

Throws

InvalidInputError If the ref, status, limit, or offset is invalid, or the ref is not on chainId.

Throws

SignerRequiredError If no account is configured.

Throws

IndexerError If the indexer read or producer-row conversion fails.

Throws

NotConfiguredError If chainId names a chain this exchange does not span, or the chain's indexer is absent or does not index that chain.


fetchPortfolioAnalytics()

fetchPortfolioAnalytics(timeframe, params?): Promise<PortfolioAnalytics>

Defined in: packages/sdk/src/unified/exchange.ts:3136

The wallet's portfolio metrics plane over a timeframe: a holdings curve, an equity curve, per-bucket PnL, money-weighted return, volume, and fees saved versus a comparison taker rate. Computed client-side from the wallet's indexed fills (avg-cost basis) marked to candle closes — no server aggregate involved.

The two curves measure different things. holdings is what the traded book is worth at each sample, so it is a level. equity is the window's cumulative realized and unrealized PnL, so it is a change. Neither counts a token that arrived without a fill: read balances from the chain for what the wallet itself holds. Read HoldingsPoint before presenting the level — it states where its sign and its completeness end.

SPOT-scoped today: binary outcomes settle rather than mark, and the perp account plane (funding, margin) joins the fold as new event kinds when perp analytics land. The indexer returns only the wallet's SPOT fills, so binary and perp rows are not transferred. Fills are paged to exhaustion — truncating would drop the OLDEST fills and silently corrupt the carried-in cost basis, not just undercount volume. Chains are read concurrently, one page in flight per chain. An optional page budget counts pages across all chains and rejects with an incomplete-history error instead of returning partial PnL. A failure on one chain rejects the call, and on a multi-chain exchange an indexer failure carries that chainId. Each call starts at the newest page, including after a previous empty result. Fills whose taker direction the indexer has not resolved (takerIsBid null), or where the wallet's role (maker vs taker) is unknowable, are skipped rather than guessed.

The money-weighted return needs to know what capital the wallet put in. Fills alone cannot say — capital that never passed through a trade is invisible to them — so without funding the capital base is a trades-only proxy that overstates the return for a wallet trading a small part of its balance. Pass funding to measure against real external capital, and read mwrr.capitalBasis to see which definition applied.

Example (Measuring portfolio performance)

ts
const p = await exchange.fetchPortfolioAnalytics("7d");
console.log(p.pnl.totalUsd, p.mwrr.return, p.equity.length);

Parameters

timeframe

PortfolioTimeframe

params?

SomniaMarketsFetchPortfolioAnalyticsOptions = {}

Returns

Promise<PortfolioAnalytics>

Throws

InvalidInputError If the optional page budget is invalid.

Throws

SignerRequiredError If no account is configured.

Throws

IndexerError If an indexer read fails.

Throws

IndexerUnavailableError If the SDK refuses the indexer read locally.

Throws

FillHistoryUnavailableError If the indexer cannot identify the served fill dataset.

Throws

FillHistoryRestartRequiredError If the fill dataset changes while history is paged.

Throws

IncompleteHistoryError If the budget ends or a cursor cycles before complete history is read.

Throws

NotConfiguredError If the chain's indexer is absent or does not index that chain.


fetchMyTrades()

fetchMyTrades(ref?, since?, limit?, filter?): Promise<UnifiedTrade[]>

Defined in: packages/sdk/src/unified/exchange.ts:3337

My historical trades, newest-first across every venue.

Details

Reads the unified fill tape (getUserFills), so the scope, the window and the limit are applied by the INDEXER rather than to an already-truncated page. This is what makes a narrow question answerable: a ref-scoped call returns that market's fills however old they are, where a per-venue read would have capped at its newest 50 across all markets first and left nothing to filter.

limit counts rows YOU receive. A fill on a market outside the loaded registry is hydrated by its market id, so a settled market's fills keep their own symbol; only a row the indexer has no market for at all is dropped. The read therefore pages until it has limit resolvable rows or the tape runs out — asking the query for exactly limit would under-deliver by however many it then dropped.

  • ref: Restrict to one tradable (symbol or address). Omit for all.
  • since: Lower time bound, milliseconds — same clock as UnifiedTrade.timestamp, so a value read off a previous row can be passed straight back. Converted to the indexer's unix seconds internally.
  • limit: Max rows to return (default 50).
  • filter.chainId: Read only this chain. With no ref and no filter.chainId this reads every chain and throws when any chain fails; on an exchange with more than one chain the thrown IndexerError then has chainId set to the chain that failed. A read over more than one chain skips a chain bound with no indexerUrl (see chainIdsWithoutIndexer).

Parameters

ref?

MarketHandle

since?

number

limit?

number = 50

filter?

SomniaMarketsChainFilter = {}

Returns

Promise<UnifiedTrade[]>

Throws

InvalidInputError If the symbol or source position is invalid, or ref is not on filter.chainId.

Throws

IndexerError If the indexer read fails.

Throws

SignerRequiredError If no account is configured.

Throws

NotConfiguredError If filter.chainId names a chain this exchange does not span, or the chain's indexer is absent or does not index that chain.


fetchStatus()

fetchStatus(): Promise<ExchangeStatus>

Defined in: packages/sdk/src/unified/exchange.ts:3472

Read the local chain-tail status without network requests. "ok" means no watches are held or the tail is connected. "connecting" means a watch is awaiting its first head. "error" means a previously live socket was lost or the tail retains an operational failure.

Returns

Promise<ExchangeStatus>


fetchDataStatus()

fetchDataStatus(): Promise<ExchangeDataStatus>

Defined in: packages/sdk/src/unified/exchange.ts:3508

Observe independent indexer freshness and per-asset price health once. No polling starts. The top-level verdict describes only the chain tails. indexer is the PRIMARY chain's freshness; indexerByChain carries every configured chain's. Use fetchStatus for a local-only snapshot.

This is the health surface, so it measures each chain on its own. When one chain's measurement fails, the call still resolves: that chain's error is in indexerErrorByChain and the chain has no key in indexerByChain. The call throws only when every configured chain fails. It then throws the primary chain's error. On an exchange with more than one chain, that IndexerError or RpcError has chainId set to the primary chain.

Returns

Promise<ExchangeDataStatus>

Throws

Configured chain or metadata arguments are invalid, on every chain.

Throws

Indexer metadata could not be read, on every chain.

Throws

The owner has no chain transport, on every chain.

Throws

Independent chain measurement failed, on every chain.


watchOrderBook()

watchOrderBook(ref, limit?): Promise<UnifiedOrderBook>

Defined in: packages/sdk/src/unified/exchange.ts:3661

Streaming book off the local store: zero round-trips, current to the last block; each await resolves on the next book change.

Example (Watching the order book)

A quoting loop: wake on every book change, read the touch.

ts
while (true) {
  const book = await exchange.watchOrderBook("SOMI/USDC", 5);
  const [bestBid] = book.bids[0] ?? [];
  const [bestAsk] = book.asks[0] ?? [];
  console.log(`bid ${bestBid} / ask ${bestAsk}`);
}

Block provenance comes from the native owner. Background failures appear in tail status.

Parameters

ref

MarketHandle

limit?

number = 10

Returns

Promise<UnifiedOrderBook>

Throws

InvalidInputError If the symbol or source position is invalid.

Throws

NotConfiguredError If the owner has no chain endpoint.

Throws

IndexerError If snapshot initialization fails.

Throws

RpcError If chain catch-up fails.

Throws

ContractRevertError If a required contract read reverts.

Throws

InvalidInputError If the market is a SETTLED binary market (resolved, voided or finalized). Its pool now serves the next market in the series, so only redeem, balances and history remain.

Throws

A DOMException named AbortError if SomniaMarkets.unwatchMarket or SomniaMarkets.close releases the market while this call waits, or while it hydrates and the hydration succeeds. This is caller cancellation.


watchTrades()

watchTrades(ref, limit?): Promise<UnifiedTrade[]>

Defined in: packages/sdk/src/unified/exchange.ts:3705

Streaming public trades (the live tape), newest first.

Example (Watching trades)

Print each fill as it lands ([0] is always the latest).

ts
while (true) {
  const [latest] = await exchange.watchTrades("SOMI/USDC", 1);
  if (latest) console.log(`${latest.side ?? "?"} ${latest.amount} @ ${latest.price}`);
}

Block provenance comes from the native owner. Background failures appear in tail status.

Parameters

ref

MarketHandle

limit?

number = 50

Returns

Promise<UnifiedTrade[]>

Throws

InvalidInputError If the symbol or source position is invalid.

Throws

NotConfiguredError If the owner has no chain endpoint.

Throws

IndexerError If snapshot initialization fails.

Throws

RpcError If chain catch-up fails.

Throws

ContractRevertError If a required contract read reverts.

Throws

InvalidInputError If the market is a SETTLED binary market (resolved, voided or finalized). Its pool now serves the next market in the series, so only redeem, balances and history remain.

Throws

A DOMException named AbortError if SomniaMarkets.unwatchMarket or SomniaMarkets.close releases the market while this call waits, or while it hydrates and the hydration succeeds. This is caller cancellation.


watchOrders()

watchOrders(ref, limit?): Promise<UnifiedOrder[]>

Defined in: packages/sdk/src/unified/exchange.ts:3766

Streaming view of MY orders on this tradable (authenticated).

When to use

Use to learn that a resting order filled: its status flips to "closed".

Example (Waiting for an order)

Place a limit order, then block until it fully fills (or dies).

ts
const placed = await exchange.createOrder(symbol, "limit", "buy", 10, 0.62);
while (placed.status === "open") {
  const orders = await exchange.watchOrders(symbol); // resolves on the next change
  const mine = orders.find((o) => o.id === placed.id);
  if (!mine || mine.status !== "open") break; // filled, canceled, or expired
}
  • Throws InvalidInputError - the market is a SETTLED binary market (resolved, voided or finalized). Its pool now serves the next market in the series, so only redeem, balances and history remain.
  • Throws a DOMException named AbortError - SomniaMarkets.unwatchMarket or SomniaMarkets.close released the market while this call waited, or while it hydrated and the hydration succeeded. This is caller cancellation.

Parameters

ref

MarketHandle

limit?

number = 100

Returns

Promise<UnifiedOrder[]>


watchMyTrades()

watchMyTrades(ref, limit?): Promise<UnifiedTrade[]>

Defined in: packages/sdk/src/unified/exchange.ts:3819

Streaming view of MY fills on this tradable (authenticated). Block provenance comes from the native owner. Background failures appear in tail status.

Parameters

ref

MarketHandle

limit?

number = 50

Returns

Promise<UnifiedTrade[]>

Throws

InvalidInputError If the symbol or source position is invalid.

Throws

NotConfiguredError If the owner has no chain endpoint.

Throws

IndexerError If snapshot initialization fails.

Throws

RpcError If chain catch-up fails.

Throws

ContractRevertError If a required contract read reverts.

Throws

SignerRequiredError If no account is configured.

Throws

InvalidInputError If the market is a SETTLED binary market (resolved, voided or finalized). Its pool now serves the next market in the series, so only redeem, balances and history remain.

Throws

A DOMException named AbortError if SomniaMarkets.unwatchMarket or SomniaMarkets.close releases the market while this call waits, or while it hydrates and the hydration succeeds. This is caller cancellation.


unwatchMarket()

unwatchMarket(ref): Promise<void>

Defined in: packages/sdk/src/unified/exchange.ts:3910

Release one market's streaming resources and keep the exchange open.

When to use

Use when a consumer stops following a market, for example a bot that moves to the next symbol in a rotating binary series. Without this call the exchange holds every watched market until SomniaMarkets.close.

Details

The first watchOrderBook, watchTrades, watchOrders or watchMyTrades call on a market opens one market watch. The other calls on that market share it. This call releases that watch and every streaming channel on the market, for all its outcomes. Other markets are not affected.

  • A pending watch* wait on the market rejects with a DOMException named AbortError. This is caller cancellation, not an SDK failure.
  • A watch* call still hydrating the market rejects with that AbortError when hydration succeeds, and its watch is released then. If hydration fails, the call rejects with the hydration error instead.
  • The next watch* call on the market opens a fresh watch. A late hydration from before the release cannot replace that watch or stop it.
  • The client keeps the market's live data for a 30-second linger after the last holder releases it, so a quick rewatch skips a new snapshot. Then it drops the market's orders and fills. It keeps the market row.
  • Releasing a market that is not watched does nothing. Calling this twice is the same as calling it once, and it is safe before or after close().

The cache entry and the channels are released before this returns. The returned promise resolves when the watch handle is stopped: at once for a hydrated watch, or when a hydration in progress settles. Awaiting it is optional.

A symbol that no longer resolves, for example one that a later loadMarkets(true) dropped, is still released by its market symbol. An on-chain ref (an address, a market id or a { chainId, id } ref) must resolve: it names no symbol, so an ambiguous or unknown one rejects. Price watches from watchPrice are not affected; close() releases them.

Example (Following a rotating series)

ts
let current: string | undefined;
async function follow(next: string) {
  if (current) await exchange.unwatchMarket(current);
  current = next;
  return exchange.watchOrderBook(next, 5);
}

Parameters

ref

MarketHandle

The market or any of its tradables, in any form SomniaMarkets.market accepts.

Returns

Promise<void>

Throws

InvalidInputError If ref is an on-chain ref (an address, a market id or a { chainId, id } ref) that does not resolve to exactly one market.


watchUser()

watchUser(filter?): Promise<WatchHandle>

Defined in: packages/sdk/src/unified/exchange.ts:3955

Hydrate and hold the signer's order/fill history on EVERY configured chain, behind one handle. Each chain's engine opens its own ref-counted user watch; stopping the handle releases all of them. Activity then arrives on the per-market watch* verbs with each event's chainId.

Gotchas

  • Throws SignerRequiredError - no signer on this exchange.
  • Throws NotConfiguredError - filter.chainId names a chain this exchange does not span, or the one chain the watch touches has no indexer.
  • Throws RpcError - a chain's socket could not open. The chains open at the same time. The call waits for every chain to settle, then stops each handle that opened, including one that opened after the failure, before the error propagates. When more than one chain fails, the error is the one from the first failing chain in binding order. On an exchange with more than one chain and no filter.chainId, its chainId is set to that chain.

Pass filter.chainId to watch one chain only (see SomniaMarketsChainFilter). A watch over more than one chain skips a chain bound with no indexerUrl (see chainIdsWithoutIndexer), because a user watch loads its history from the indexer.

Parameters

filter?

SomniaMarketsChainFilter = {}

Returns

Promise<WatchHandle>


ensureFunds()

ensureFunds(params, options?): Promise<ExchangeBridgeResult | null>

Defined in: packages/sdk/src/unified/exchange.ts:4092

Make sure the account holds amount of a currency on one chain, bridging the shortfall from the richest other configured chain when it does not. createOrder(…, { funding: "auto" }) bridges the same way, but it finds the order's escrow token by address instead of by code.

Details

  • code: the currency's code — the key fetchBalance uses ("USDso", "STT", …). Addresses differ per chain; the code names the asset. On chainId it resolves to the native coin, else the bridge registry's token for that code, else a listed market's token with that code. The bridge route then follows that token's address, never the code.
  • chainId: the chain the funds must end up on.
  • amount: raw units of the currency on that chain.

Resolves with null when no new bridge leg was needed, else the bridge result. Other-chain read failures remain available through collateralReadErrors. A shortfall error also retains them in readErrors and its aggregate cause.

Calls for the same account, chain and currency run one at a time on this exchange, so two concurrent calls do not both bridge the same shortfall. A createOrder(…, { funding: "auto" }) for the same key keeps its place in that queue until its order is sent, so the next call reads the balance after that order took its escrow. Each call in the queue can wait a full delivery timeout, so waits add up.

A leg that timed out stays pending on this exchange. When a later call for the same key finds a shortfall, it waits for that message first and sends no new leg for it. When the balance already covers the amount, it does not wait. This state is in memory only: a new exchange instance does not know about an earlier leg.

close() ends the delivery waits of every call that started before it, including calls still in the queue. Such a call opens no new wait. A leg it sends is reported as pending.

Gotchas

  • Throws CollateralOnWrongChainError - no other configured chain holds enough, or the currency has no bridge route.
  • Throws InvalidInputError - amount is positive and no token on chainId carries code.
  • Throws BridgeLiquidityError - a native route's destination router holds less than the shortfall. Nothing was sent.
  • Throws BridgeDeliveryTimeoutError - a bridge leg was not delivered within the timeout or before close(). The leg is the one this call sent, or a pending leg from an earlier call, in which case this call sent nothing. The error carries messageId. A later call on this exchange waits for that message again and sends no new leg for it. From another process or instance, check delivered(messageId) on the destination Mailbox first, or the funds move twice.
  • Throws RpcError - a balance read or a bridge step failed. A failed destination router read (operation: "getBalance") sent nothing. See ExchangeBridge.transfer for the other bridge RpcError cases.

Parameters

params
code

string

chainId

number

amount

bigint

options?

ExchangeBridgeTransferOptions = {}

Returns

Promise<ExchangeBridgeResult | null>


watchPrice()

watchPrice(asset): Promise<UnifiedPrice>

Defined in: packages/sdk/src/unified/exchange.ts:4488

Streaming price off the local price store: zero round-trips, current to the last pushed tick; each await resolves on the next price change.

Details

First call hydrates the ref-counted feed watch.

Gotchas

Requires config.priceFeed to be set, INCLUDING its quote: an unpinned feed rejects a live watch with a NotConfiguredError naming config.priceFeed.quote, because the tick tape's cursor is unique only within one quote. The one-shot price reads still work unpinned.

Parameters

asset

string

Returns

Promise<UnifiedPrice>


fetchPrice()

fetchPrice(asset): Promise<UnifiedPrice | null>

Defined in: packages/sdk/src/unified/exchange.ts:4511

One-shot current price (indexer HTTP read; no watch needed), or null if the feed has no observations yet.

Parameters

asset

string

Returns

Promise<UnifiedPrice | null>


fetchPriceOHLCV()

fetchPriceOHLCV(asset, timeframe?, since?, limit?): Promise<UnifiedOHLCV[]>

Defined in: packages/sdk/src/unified/exchange.ts:4527

OHLC price candles (EMA oracle), oldest first as [ms,o,h,l,c,vol] rows.

Details

Timeframes: 1m 1h 1d (aliases for the feed's M1/H1/D1).

Gotchas

vol is the oracle update count for the bucket (NOT trade volume).

Parameters

asset

string

timeframe?

string = "1m"

since?

number

limit?

number = 500

Returns

Promise<UnifiedOHLCV[]>


createOrder()

createOrder(ref, type, side, amount, price?, params?): Promise<UnifiedOrder>

Defined in: packages/sdk/src/unified/exchange.ts:4600

Place an order.

Details

Works identically for every market kind: the tradable symbol carries the outcome, side is plain buy/sell, prices and amounts are human units in the tradable's own terms. type: "market" computes a crossing limit from the best opposite level ± params.slippage (default 1%) and sends it IOC. Resolves once mined, with fills decoded from the same round-trip.

Gotchas

A NO price is the NO probability — the YES-terms complement is handled internally.

The price and quantity are ALIGNED to the market's tick and lot grids before they are sent, because the pool rejects an off-grid value outright. Alignment never moves a value against you: a buy price rounds down, a sell price rounds up, and a quantity always rounds down, so the order is never larger or worse priced than you asked for. The returned UnifiedOrder carries what was actually placed, which may differ from the arguments by up to one tick or lot — read price and amount back from it rather than assuming your inputs. Pre-aligning with priceToPrecision / amountToPrecision makes this a no-op, since aligning an aligned value changes nothing.

Note priceToPrecision always rounds DOWN, for either side; this path is side-aware instead, so for a sell the two can differ by one tick.

A quantity below one whole lot throws InvalidInputError rather than silently placing a zero-quantity order.

  • Throws SignerRequiredError - the exchange was built without a privateKey / account / walletClient.
  • Throws InvalidInputError - unknown symbol (call loadMarkets() first), a "limit" order with no price, or a "market" order whose opposite book side is empty so no crossing price exists.
  • Throws ContractRevertError - the chain rejected the order. Branch on errorName for the protocol's own reason (e.g. InsufficientBalance, ExpiredOrderMustBeCancelled). Also thrown before anything is sent when a spot pre-check read (getAutoPullRequirement, getManualVaultMode) reverts.
  • Throws RpcError - the node refused the transaction, or a pre-check read (pool requirement, vault mode, balance) failed before anything was sent.
  • Throws TransactionOutcomeUnknownError - the transaction may have reached the node and its receipt was not observed. Look up hash before sending again.
  • Throws CollateralOnWrongChainError - the collateral pre-check (see funding) found a shortfall on the market's chain: in the wallet, or in the pool vault under manual vault mode. Nothing was sent.

Example (Placing binary orders)

Rest a bid at 62% on YES, then take the NO book at market.

ts
const rested = await exchange.createOrder("BTC-95000-31DEC26/USDC#YES", "limit", "buy", 25, 0.62);
console.log(rested.status, rested.filled); // "open" 0 — or "closed" if it crossed

const taken = await exchange.createOrder("BTC-95000-31DEC26/USDC#NO", "market", "sell", 10, undefined, {
  slippage: 0.02, // accept up to 2% past the best bid
});
  • Throws InvalidInputError - the market is a SETTLED binary market (resolved, voided or finalized). Its pool now serves the next market in the series, so only redeem, balances and history remain.
  • Throws BridgeLiquidityError - funding: "auto" needs a native bridge route whose destination router holds less than the shortfall. Nothing was sent.
  • Throws BridgeDeliveryTimeoutError - with funding: "auto", a bridge leg was not delivered within the timeout or before close(). The leg is the one this call sent, or a pending leg from an earlier call on the same account, chain and token, in which case nothing was sent. No order was placed. See SomniaMarkets.ensureFunds.
  • Throws RpcError - with funding: "auto", a balance read or the bridge failed. See SomniaMarkets.ensureFunds.

Parameters

ref

MarketHandle

type

"market" | "limit"

side

"buy" | "sell"

amount

number

price?

number

params?

CreateOrderParams = {}

Returns

Promise<UnifiedOrder>


cancelOrder()

cancelOrder(id, ref?): Promise<{ id: string; symbol: string; chainId: number; status: "canceled"; info: unknown; }>

Defined in: packages/sdk/src/unified/exchange.ts:4801

Cancel a resting order by id (from createOrder / watchOrders).

Gotchas

Example (Cancelling an open order)

ts
const placed = await exchange.createOrder("SOMI/USDC", "limit", "buy", 10, 0.55);
if (placed.status === "open") await exchange.cancelOrder(placed.id, "SOMI/USDC");

Parameters

id

string | ChainOrderRef

ref?

MarketHandle

Returns

Promise<{ id: string; symbol: string; chainId: number; status: "canceled"; info: unknown; }>


createStopOrder()

createStopOrder(ref, type, side, amount, triggerPrice, price?, params?): Promise<UnifiedStopOrder>

Defined in: packages/sdk/src/unified/exchange.ts:4892

Place a stop / take-profit order: rests OFF the book on the market's stop registry and fires as a market or limit order when the pool's mark price crosses triggerPrice. The trigger direction is inferred from which side of the current mark the trigger sits on; pass params.triggerDirection to pin it explicitly.

Gotchas

The trigger, limit price and quantity are aligned to the market's grids, and the trigger aligns AWAY from the mark so it cannot land on it (a trigger equal to the mark fires the instant it is armed). The limit price aligns like any order price — a buy down, a sell up — so it never becomes worse than stated.

Those two rules are independent, so a limit set exactly EQUAL to the trigger can end up one tick inside it: a buy stop at trigger 0.5004, limit 0.5004 on a 0.001 grid arms at 0.501 and rests a 0.500 bid, which may not fill. That is deliberate — pulling the limit up to meet the trigger would make you pay more than you asked. Set the limit a tick or two past the trigger when you want the triggered order to cross.

  • Throws SignerRequiredError - the exchange was built without a privateKey / account / walletClient.
  • Throws InvalidInputError - unknown symbol, a non-spot market, a market with no stop registry, a "limit" stop with no price, a sub-lot quantity, or no mark yet to infer the trigger direction from (pass params.triggerDirection).
  • Throws IndexerError - the pool's mark was needed (to infer the trigger direction, or to price a market stop's protective limit) and the indexer read did not complete. A limit stop with params.triggerDirection set needs no mark.
  • Throws ContractRevertError - the registry or the pool rejected the placement (or the operator grant / escrow approval that precedes it). Branch on errorName.
  • Throws RpcError - a pre-placement chain read did not complete, or the node refused a transaction.
  • Throws TransactionOutcomeUnknownError - a transaction may have reached the node and its receipt was not observed. Look up hash before sending again.

Example (Placing a stop order)

A stop-loss: sell 5 if the mark drops to 1.10.

ts
const stop = await exchange.createStopOrder("SOMI/USDC", "market", "sell", 5, 1.10);
// …later: await exchange.cancelStopOrder(stop.id, "SOMI/USDC");

Parameters

ref

MarketHandle

type

"market" | "limit"

side

"buy" | "sell"

amount

number

triggerPrice

number

price?

number

params?
triggerDirection?

"above" | "below"

Returns

Promise<UnifiedStopOrder>


fetchOpenStopOrders()

fetchOpenStopOrders(ref?, filter?): Promise<UnifiedStopOrder[]>

Defined in: packages/sdk/src/unified/exchange.ts:5019

The wallet's pending (armed, untriggered) stop orders, newest first. Scope to one tradable with ref, or to one chain with filter.chainId.

With no ref and no filter.chainId this reads every chain and throws when any chain fails. On an exchange with more than one chain, the thrown IndexerError then has chainId set to the chain that failed (see SomniaMarketsChainFilter). A read over more than one chain skips a chain bound with no indexerUrl (see chainIdsWithoutIndexer).

Parameters

ref?

MarketHandle

filter?

SomniaMarketsChainFilter = {}

Returns

Promise<UnifiedStopOrder[]>

Throws

InvalidInputError If ref is unknown, or ref is not on filter.chainId.

Throws

NotConfiguredError If filter.chainId names a chain this exchange does not span, or the one chain the read touches has no indexer.


cancelStopOrder()

cancelStopOrder(id, ref): Promise<{ id: string; symbol: string; chainId: number; status: "canceled"; info: unknown; }>

Defined in: packages/sdk/src/unified/exchange.ts:5065

Cancel a pending stop order on its registry (refunds the keeper payment). id comes from fetchOpenStopOrders.

Parameters

id

string

ref

MarketHandle

Returns

Promise<{ id: string; symbol: string; chainId: number; status: "canceled"; info: unknown; }>


fetchFundingRate()

fetchFundingRate(ref): Promise<UnifiedFundingRate>

Defined in: packages/sdk/src/unified/exchange.ts:5088

Live funding-rate + mark/index snapshot for a perp market (chain read).

Parameters

ref

MarketHandle

Returns

Promise<UnifiedFundingRate>


fetchFundingRateHistory()

fetchFundingRateHistory(ref, since?, limit?): Promise<UnifiedFundingRate[]>

Defined in: packages/sdk/src/unified/exchange.ts:5159

Historical funding rates for a perp market, oldest first (ccxt-standard shape).

Reads the INDEXED series rather than the chain: only one funding value is readable on chain at a time. Positional (symbol, since, limit) follows the ccxt convention set by fetchOHLCV, unlike the object-options readers on the client.

fundingRate is normalized to a per-8h fraction using each row's own fundingWindowSec, so the series stays consistent across a parameter change. The raw indexed row is on info for anything more specific — including spanStart / spanEnd, which matter because a row's accrual reaches BACKWARDS from its timestamp and a lazily-settled one can cover hours.

since is a CURSOR, not just a window bound: passing it walks FORWARD from that point, so the ccxt pagination idiom terminates.

Details

  • ref: market symbol or pool address
  • since: unix MILLISECONDS (ccxt convention), inclusive; acts as a forward cursor
  • limit: max rows (default 100)

Example (Paging through funding history)

ts
let since = startOfHistory;
for (;;) {
  const page = await exchange.fetchFundingRateHistory("BTC/USDSO:USDSO", since, 100);
  if (page.length === 0) break;
  consume(page);
  since = page[page.length - 1].timestamp + 1;   // advances
}

Without the forward ordering this loop spins: the underlying read pages newest-first, so narrowing the window from below still returns the newest N and since never gets past the tail. Omitting since keeps the newest-first behaviour, which is what a "latest funding" read wants — fetchOHLCV has the same split.

Parameters

ref

MarketHandle

since?

number

limit?

number

Returns

Promise<UnifiedFundingRate[]>


fetchPositions()

fetchPositions(refs?, filter?): Promise<UnifiedPosition[]>

Defined in: packages/sdk/src/unified/exchange.ts:5216

Open perp positions (authenticated; on-chain MarginBank reads). Pass symbols to scope; defaults to every loaded perp market.

Gotchas

Called with NO refs, this reads only the perps the registry loaded, so a short perp set gives a short result rather than an error. Check perpDiscoverySkipped and perpDiscoveryError before you read an empty result as "this account holds no perp positions". Passing explicit refs turns an unloaded symbol into an InvalidInputError instead.

filter.chainId reads only that chain; every ref must then be on it. Otherwise the read spans the chains of its markets and throws when any chain fails. On a read over more than one chain, and on a read with no refs and no filter.chainId on an exchange with more than one chain, the thrown IndexerError or RpcError has chainId set to the chain that failed (see SomniaMarketsChainFilter).

The pool state (mark, index, funding) is read only for a pool where the account holds a position. When that read fails for one pool, for example because its oracle reverts, the position still lists with perpState: "degraded", and markPrice and unrealizedPnl are absent. The other positions are not affected. Branch on perpState, not on the absence of markPrice.

Parameters

refs?

MarketHandle[]

filter?

SomniaMarketsChainFilter = {}

Returns

Promise<UnifiedPosition[]>

Throws

InvalidInputError If a ref is unknown or not a perp, or a ref is not on filter.chainId.

Throws

NotConfiguredError If filter.chainId names a chain this exchange does not span.


depositMargin()

depositMargin(ref, amount): Promise<{ hash: string; info: unknown; }>

Defined in: packages/sdk/src/unified/exchange.ts:5321

Deposit collateral into the perp MarginBank (human quote units, e.g. USDso). One cross-margin balance covers every perp market.

Parameters

ref

MarketHandle

amount

number

Returns

Promise<{ hash: string; info: unknown; }>


withdrawMargin()

withdrawMargin(ref, amount): Promise<{ hash: string; info: unknown; }>

Defined in: packages/sdk/src/unified/exchange.ts:5340

Withdraw free collateral from the perp MarginBank (human quote units).

Parameters

ref

MarketHandle

amount

number

Returns

Promise<{ hash: string; info: unknown; }>


mintSet()

mintSet(ref, amount): Promise<{ hash: string; info: unknown; }>

Defined in: packages/sdk/src/unified/exchange.ts:5382

Mint complete sets: amount collateral → amount of EVERY outcome.

Example (Minting complete sets)

Mint 100 sets (100 USDC → 100 YES + 100 NO), then sell the side you don't want.

ts
await exchange.mintSet("BTC-95000-31DEC26/USDC", 100);
await exchange.createOrder("BTC-95000-31DEC26/USDC#NO", "limit", "sell", 100, 0.38);
  • Throws InvalidInputError - the market is a SETTLED binary market (resolved, voided or finalized). Its pool now serves the next market in the series, so only redeem, balances and history remain.

Parameters

ref

MarketHandle

amount

number

Returns

Promise<{ hash: string; info: unknown; }>


burnSet()

burnSet(ref, amount): Promise<{ hash: string; info: unknown; }>

Defined in: packages/sdk/src/unified/exchange.ts:5401

Burn complete sets back to collateral.

  • Throws InvalidInputError - the market is a SETTLED binary market (resolved, voided or finalized). Its pool now serves the next market in the series, so only redeem, balances and history remain.

Parameters

ref

MarketHandle

amount

number

Returns

Promise<{ hash: string; info: unknown; }>


redeem()

redeem(ref, amount, options?): Promise<{ hash: string; info: unknown; }>

Defined in: packages/sdk/src/unified/exchange.ts:5463

Redeem outcome tokens for collateral after settlement. Settlement- extraction v2 routes by marketId. When the caller omits the leg, the SDK verifies the BinaryMarket state and reads its winner on-chain.

Example (Redeeming a winning position)

After resolution, redeem the winning side found in the balance map. The market has left fetchMarkets() by then (a settled series market is not tradable), but fetchBalance() still reports the holding under the market's symbol and that symbol still resolves here.

ts
const bal = await exchange.fetchBalance();
const winning = bal["BTC-95000-31DEC26/USDC#YES"]?.total ?? 0;
if (winning > 0) await exchange.redeem("BTC-95000-31DEC26/USDC", winning);

Gotchas

A VOIDED market has no winning outcome — both legs pay — so the auto-lookup cannot pick one and redeem throws. Pass { outcomeIdx } for the leg you hold, and call once per leg to claim both. An unresolved market also throws instead of guessing from an unfinalized payout vector.

Errors

  • Throws InvalidInputError for an unknown or non-binary market, an invalid amount, or an omitted leg when the on-chain market is unresolved, voided, or does not store an exact one-hot resolved payout vector.
  • Throws NotConfiguredError when the binary module address is not configured.
  • Throws SignerRequiredError when the exchange has no usable signer.
  • Throws RpcError when a market or approval read does not complete, or the node refuses a transaction.
  • Throws TransactionOutcomeUnknownError when an approval or redemption transaction may have reached the node and its receipt was not observed. Look up hash before sending again.
  • Throws ContractRevertError when a market read, operator approval, or redemption transaction is rejected by a contract.

Parameters

ref

MarketHandle

Market symbol or id.

amount

number

Outcome-token amount to burn, in display units.

options?

RedeemOptions

Optional leg selection. Omit it on a resolved market to verify the terminal state and read the winner on-chain. A voided market requires options.outcomeIdx because both legs are redeemable and only the caller knows which one they hold.

Returns

Promise<{ hash: string; info: unknown; }>


close()

close(): Promise<void>

Defined in: packages/sdk/src/unified/exchange.ts:5541

Release every watch, channel, socket, and timer this exchange holds and stop the client's live machinery.

Details

This includes the chain WebSocket, so a Node process that has touched the chain exits on its own once this resolves. The instance stays usable for one-shot fetch calls: a later read reopens a connection, and a later close() releases that one too.

A pending watch* wait, and a watch* call whose market hydration succeeds after this call starts, rejects with a DOMException named AbortError. A hydration that fails rejects with its own error. To release one market and keep the rest, use SomniaMarkets.unwatchMarket.

It also ends the bridge delivery waits of every bridge.transfer, ensureFunds and auto-funded createOrder that started before it, including calls still queued for funding. Such a call opens no new wait. bridge.transfer resolves with delivered: false, and ensureFunds throws BridgeDeliveryTimeoutError. The leg stays pending, and the next ensureFunds for its key waits for it on a shortfall.

Gotchas

Each exchange owns its own chain socket, even when several use the same wsRpcUrl, so closing one never affects another. A viem client taken from getViemClient() before this call belongs to the closed connection: its requests reject with viem's SocketClosedError.

Returns

Promise<void>