@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
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
readonlyhas: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
readonlyfetchMarkets:true=true
fetchOrderBook
readonlyfetchOrderBook:true=true
fetchTrades
readonlyfetchTrades:true=true
fetchOHLCV
readonlyfetchOHLCV:true=true
fetchBalance
readonlyfetchBalance:true=true
fetchOpenOrders
readonlyfetchOpenOrders:true=true
getOrderHistoryPage
readonlygetOrderHistoryPage:true=true
SomniaMarkets.getOrderHistoryPage
getOrdersPage
readonlygetOrdersPage:true=true
fetchMyTrades
readonlyfetchMyTrades:true=true
fetchStatus
readonlyfetchStatus:true=true
createOrder
readonlycreateOrder:true=true
cancelOrder
readonlycancelOrder:true=true
watchOrderBook
readonlywatchOrderBook:true=true
watchTrades
readonlywatchTrades:true=true
watchOrders
readonlywatchOrders:true=true
watchMyTrades
readonlywatchMyTrades:true=true
fetchPositions
readonlyfetchPositions:true=true
fetchFundingRate
readonlyfetchFundingRate:true=true
SomniaMarkets.fetchFundingRate
fetchFundingRateHistory
readonlyfetchFundingRateHistory:true=true
SomniaMarkets.fetchFundingRateHistory — the key did not previously exist in this map, so it had to be ADDED rather than flipped.
watchPrice
readonlywatchPrice:true=true
fetchPrice
readonlyfetchPrice:true=true
fetchPriceOHLCV
readonlyfetchPriceOHLCV:true=true
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:
- fetchBalance keeps their ERC-20 and native balances and skips their binary outcome holdings.
- fetchOpenOrders reads their resting orders from the chain, over the perp pools that loadMarkets discovered on that chain.
- fetchOrders, getOrdersPage, fetchMyTrades, fetchOpenStopOrders and watchUser skip these chains.
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
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
chainsconfig. perpDiscoveryErrorsByChain reports every failing chain. - Do NOT infer this from
perpStatusbeing 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. nulldoes not mean the venue has perps. A chain with no perps plane deployed (local anvil) never attempts discovery and reportsnulltoo. 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)
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
nullfor 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.getIndexerFreshnessis the general signal for lag.
Example (Telling "no perps here" apart from "could not look")
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
chainsconfig. 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
listRegistryMarketsCheckedthroughRegistrySweep.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)
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
indexerUrlalways 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
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
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
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
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 themarginBankoff 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'sMarketCreatedevents. - Spot — indexer-only.
SpotPoolRegistryanswers "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,fetchMyTradesorfetchPositions, 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 bareloadMarkets(),fetchMarkets(),ensureFundsandloadMarkets(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
indexerUrlrecords 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
wsRpcUrlnor a chain whoserpcUrlscarry 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
Returns
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
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
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
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
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.
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
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.
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
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.chainIdnames 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. TheloadMarkets()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
balanceOfreverted.
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.
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?
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?
limit?
number
filter?
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.
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?
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)
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)
const p = await exchange.fetchPortfolioAnalytics("7d");
console.log(p.pnl.totalUsd, p.mwrr.return, p.equity.length);
Parameters
timeframe
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 norefand nofilter.chainIdthis reads every chain and throws when any chain fails; on an exchange with more than one chain the thrownIndexerErrorthen haschainIdset to the chain that failed. A read over more than one chain skips a chain bound with noindexerUrl(see chainIdsWithoutIndexer).
Parameters
ref?
since?
number
limit?
number = 50
filter?
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.
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
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).
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
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).
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
DOMExceptionnamedAbortError- 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
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
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 aDOMExceptionnamedAbortError. This is caller cancellation, not an SDK failure. - A
watch*call still hydrating the market rejects with thatAbortErrorwhen 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)
let current: string | undefined;
async function follow(next: string) {
if (current) await exchange.unwatchMarket(current);
current = next;
return exchange.watchOrderBook(next, 5);
}
Parameters
ref
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.chainIdnames 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, itschainIdis 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?
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. OnchainIdit 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 -
amountis positive and no token onchainIdcarriescode. - 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 carriesmessageId. A later call on this exchange waits for that message again and sends no new leg for it. From another process or instance, checkdelivered(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 bridgeRpcErrorcases.
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
errorNamefor 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
hashbefore 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.
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 beforeclose(). 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
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
- Throws SignerRequiredError - no signer on this exchange.
- Throws InvalidInputError - unknown symbol.
- Throws ContractRevertError - the cancel did not land;
errorNamesays why (an already-filled or already-canceled order reverts). - Throws RpcError - a request did not complete, or the node refused the transaction.
- Throws TransactionOutcomeUnknownError - the transaction may have reached the node and its receipt was not observed. Look up
hashbefore sending again.
Example (Cancelling an open order)
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?
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 (passparams.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.triggerDirectionset 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
hashbefore sending again.
Example (Placing a stop order)
A stop-loss: sell 5 if the mark drops to 1.10.
const stop = await exchange.createStopOrder("SOMI/USDC", "market", "sell", 5, 1.10);
// …later: await exchange.cancelStopOrder(stop.id, "SOMI/USDC");
Parameters
ref
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?
filter?
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
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
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 addresssince: unix MILLISECONDS (ccxt convention), inclusive; acts as a forward cursorlimit: max rows (default 100)
Example (Paging through funding history)
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
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?
filter?
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
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
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.
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
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
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.
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
hashbefore sending again. - Throws ContractRevertError when a market read, operator approval, or redemption transaction is rejected by a contract.
Parameters
ref
Market symbol or id.
amount
number
Outcome-token amount to burn, in display units.
options?
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>