Somnia Markets / TypeScript SDK
Binary markets
A binary market is an order book over a YES/NO question ("Will BTC be above
$95k at expiry?"). Each market has its own BinaryMarket contract (lifecycle,
resolution) and uses a BinaryPool (the CLOB + escrow) that can host later
markets. Its YES and NO outcome tokens are backed 1:1 by collateral. This guide
covers reading and trading them; pool recycling
explains why the pool address does not identify one binary market. The shared
client mechanics (watches, read tiers, signers) are in
the engine guide.
The mental model
- Prices are YES probabilities. Every price in the API is the YES price in
raw collateral units per whole outcome token —
620000with 6-decimal collateral means 0.62, i.e. a 62% implied probability. Convert withprobabilityToPrice/priceToProbability, andfromHuman/toHumanat the UI edges, passing the selected market'squoteDecimals. On the active mainnet venue USDso has 18 decimals; on Shannon and Hideki tUSDC has 6. The NO side is always the complement: a NO at 0.38 is a YES at 0.62. The pool keeps one book in YES terms. - Four sides, one book.
BinarySideisBUY_YES | SELL_YES | BUY_NO | SELL_NO. Buys escrow collateral; sells escrow the outcome token you're selling. Opposite-side orders can match by minting a fresh YES+NO pair from collateral (two buyers) or burning one back to collateral (two sellers) — theBinaryFillKindon a fill tells you which. - Lifecycle.
Listed → Trading → Locked → Settling → Resolved | Voided → Finalized(BinaryMarketStatus;Finalizedonce the settlement snapshot is swept and the pool released). Trading is only possible inTrading; after resolution the winning token redeems 1:1 for collateral, net of the venue's one-time settlement fee if it configured one. A voided market pays BOTH sides instead, from the payout vector it stored at void time, never fee'd — so redeeming one names the leg (see Complete sets and settlement). - Markets roll themselves. Each cadence (1m / 5m / 15m / 1h / 4h / 24h) is a series
that a
MarketCreatorcontract advances autonomously — at every wall-clock boundary it creates the nextBinaryMarket, uses a releasedBinaryPoolwhen available or deploys a new one, and schedules the oracle question. Series markets are reference-mode: there is no fixed strike (the event'sstrikeis always 0) — a market resolves YES iff its own oracle answer is at/above the previous boundary's reference answer. Markets can also be created directly viaBinaryMarketsModule.createMarketunder an operator's venue (these can be bucket-mode, with a real strike). Discovery is event-driven either way: an all-markets watch withdiscoverpicks each new market up from the creation events (see below). You don't need to create markets — you watch, trade, and redeem them.
The series, and how a market is born
MarketCreator runs the whole protocol off one Somnia reactivity Schedule
subscription (the precompile at 0x0100). When it fires, a single callback
drains that boundary's whole batch of due series in a gas-bounded loop —
rolling series until the gas envelope runs low — then re-arms the Schedule
sub for the next-earliest pending boundary. A batch too large for one callback
(the 00:00-UTC collision, where every cadence lands at once) saves a resume
index and arms a sub for the next block to finish the spill. Exactly one
subscription is ever live. (Historical note: an earlier one-series-per-hop
RollNext reentrancy chain was tried and removed — on live Somnia the
self-emitted event wasn't re-queued after a heavy roll, so it stalled after one
hop. The gas-bounded loop with next-block spill replaced it.)
Each roll emits, from the MarketCreator:
MarketCreated(
bytes32 marketId, address market, address pool,
uint256 yesId, uint256 noId, address collateral, string asset,
uint256 strike, // always 0 — series markets are reference-mode (no fixed strike)
uint64 tradingStart, uint64 expiry,
uint256 oracleQuestionId, string question,
uint64 intervalSec // series cadence: 60 / 300 / 900 / 3600 / 14400 / 86400
)
Every market — series-rolled or not — ALSO fires the BinaryMarketsModule's
own 20-field MarketCreated, the only creation event carrying the
(operatorId, venueId) origin attribution. A discovery watch
(watchMarkets({ discover: true })) listens to both, so module-created markets
that never pass through a MarketCreator join the watch live too.
Read cadence from
intervalSec, and treat it as approximate. A series' first market is a bootstrap partial (trigger time → the next aligned boundary), so its window is shorter than the cadence. On the SDK'sBinaryMarkettype the cadence surfaces asintervalSec?: string | null(nullwhen a market predates the field).For a
MarketCreator-rolled market the indexer patches the exact cadence fromMarketCreator.MarketCreated.intervalSec, so the field joins by equality. Module-created markets, and rows indexed before that patch existed, still carry the window-derivedexpiry − tradingStart, which a late roll shortens to 898s or 899s on a 15m series.BinaryMarketFilter.intervalSecbands the value (±CADENCE_TOLERANCE_SEC) andsnapToCadencegroups by the same rule, so both kinds of row match; use those rather than comparing the raw field.
Resolution is a separate loop. OracleCore emits
AnswerPosted(uint256,uint8,string,int256,bool,uint8,uint256[]) when the
oracle answers a question. OracleHub subscribes to that event on the
OracleCore address and routes the answer through BinaryMarketsModule.
The SDK sees a Resolved or Voided lifecycle event from the individual
BinaryMarket — gate redemption on that, per the rule below. Permissionless
voidExpired() can also emit Voided without an oracle answer.
Event emitters
For a raw log or Reactivity subscription, filter by the contract that emits
the event. The module, settlement contract, market and pool are separate log
sources. Use config.addresses for configured singletons on the selected
chain, the selected market's marketAddress and poolAddress for clones, and
smart-contracts/deployments/<chainId>/<environment>/addresses.json for
OracleCore. An existing receipt's log.address identifies its actual
emitter. The SDK's
SOMNIA_TESTNET_ADDRESSES and SOMNIA_MAINNET_ADDRESSES are generated from
their respective deployment manifests. marketCreator names a configured
creator; other venues can have their own creator, available as creator on the
module's MarketCreated event. For historical AnswerPosted logs, select the
OracleCore deployment that emitted those answers if the active core changed.
| Event signature | Emitting contract | Filter address |
|---|---|---|
MarketCreated (series, 13 arguments) |
MarketCreator on a series roll |
Venue's marketCreator |
MarketCreated (module, 20 arguments) |
BinaryMarketsModule on every creation, including a series roll |
binaryModule |
AnswerPosted(uint256,uint8,string,int256,bool,uint8,uint256[]) |
OracleCore, not OracleHub |
Matching manifest's OracleCore.address |
Resolved(uint32,uint256[]) / Voided() |
Individual BinaryMarket |
Market's marketAddress |
MarketFinalized(bytes32,address,uint256) |
BinaryMarketsModule; carries marketId |
binaryModule |
MarketFinalized(uint256,address,uint64,address,uint256,bool,uint256[]) |
BinarySettlement; carries netBacking |
binarySettlement |
PoolFinalized(uint64,uint256) |
Individual BinaryPool; its backing was swept |
Market's poolAddress and indexed marketNonce |
PoolReleased(bytes32,address,address) |
BinaryMarketsModule; the pool can be reused |
binaryModule |
Redeemed(uint256,address,address,uint8,uint256,uint256) |
BinarySettlement; tokens burned; collateral paid or owed, possibly zero |
binarySettlement |
A series roll can produce both MarketCreated events for one market; deduplicate
by marketId. Their canonical signatures for topic0 are:
MarketCreated(bytes32,address,address,uint256,uint256,address,string,uint256,uint64,uint64,uint256,string,uint64)
MarketCreated(bytes32,address,address,uint256,uint32,bytes32,address,address,uint256,uint256,uint64,uint8,uint8,uint64,uint64,uint8,string,uint256,string,bytes)
A pool can serve later markets, so its address alone does not identify one
market's PoolFinalized: match the selected market's nonce to the event's
indexed marketNonce (topic1).
The first MarketFinalized payload is (marketId, pool, marketKey); the second
is (marketKey, pool, nonce, collateralToken, netBacking, voided, payoutNumerators). Their topic0 values differ. On Shannon,
a subscriber for settlement netBacking must filter the settlement event at
SOMNIA_TESTNET_ADDRESSES.binarySettlement, not the three-field module event at
SOMNIA_TESTNET_ADDRESSES.binaryModule. The address is the settlement proxy
(0xbF4a49e0Dfd092e5FBE8E5761064C49533e6Ed23 in the Shannon
50312/testnet-development manifest), not its implementation or the pool. See
Reactivity for watch and subscribe filter fields.
Oracle question links
An oracleQuestionId is scoped to its oracle deployment. Choose the host
from the market's chain ID, not from the numeric ID:
| Market chain | Question page |
|---|---|
| Shannon testnet (50312) | https://dev.oracle.somnia.host/questions/<oracleQuestionId> |
| Somnia mainnet (5031) | https://prd.oracle.somnia.host/questions/<oracleQuestionId> |
For example, a Shannon market's ID belongs at
https://dev.oracle.somnia.host/questions/<oracleQuestionId>; a mainnet ID
belongs at the production host. The same number is not a cross-host key.
These are oracle question pages; the price-feed.*.oracle.somnia.host
endpoints in the network reference
serve price-feed GraphQL instead. For another chain, verify its oracle host
before constructing a link.
Reading
Watch the market, then read the live store — synchronous, zero round-trips, current to the last block:
const watch = await client.watchMarket(market.poolAddress);
const book = client.getLiveBinaryOrderBookByMarket(market.id, { depth: 11 });
// { yesBids, yesAsks, noBids, noAsks } — NO sides derived as 1 − yesPrice
const tape = client.getLiveFills(market.poolAddress, { limit: 40 });
const live = client.getLiveMarketByPool(market.poolAddress); // status, lastPrice, volumes
The book read above uses market.id so a page for an older market shows an
empty book after its pool is recycled. The live market read describes the
pool's current occupant. The pool-keyed tape can include fills from successive
markets on that address; it does not identify one market. For a chain snapshot,
getBinaryOrderBook(pool) also reads the pool's current book without a
market-id check. See pool recycling
before using a pool address to read positions or history.
In React, mount useWatchMarketResult(pool) or another pool-keyed watch hook
alongside useLiveBinaryOrderBookByMarket(marketId) when the selected market is
not yet in the live store. The market-scoped hook can watch the pool itself only
after the market is known; it keeps an older market's book empty after recycle.
useLiveBinaryOrderBook(pool) and useLiveMarketByPool(pool) describe the
current occupant. Pool-keyed useLiveFills(pool) and
useLiveUserOrders(pool, account) can include activity from successive markets.
Discovery and history come from the indexer tier:
listLiveBinaryMarkets() / listPastBinaryMarkets({ limit, offset }) for the
market lists, getCandles(pool, interval) for charts, getPortfolio(account) (trades
default to the last seven days — tradesSince echoes the bound; pass since to widen)
for a wallet's positions + orders + trades in one round-trip.
To start a selected binary book when the indexer is down, pass
chainSeedPools={[{ chainId, pool }]} to SomniaMarketsProvider. Every
pool-watching hook under that provider uses the same chain seed policy, even
when a child hook mounts first. The provider reads the pool's current market
binding from chain and requires addresses.binaryModule on that chain's client.
Set this list before mounting the provider; remount to change it. Unlisted spot
and funding pools retain their normal indexer seed. Read usePoolSeedStatus(pool)
for that pool's seedSource and fillsLoaded: a chain-seeded book can be live
while fill history remains unloaded. Discovery, candles, portfolio and other
indexed history still need the indexer.
listLiveBinaryMarkets() with no argument returns every live market; pass a
filter to narrow (all fields optional, applied server-side):
await client.listLiveBinaryMarkets(); // all live markets
await client.listLiveBinaryMarkets({ operatorId: 1 }); // one operator's live board
await client.listLiveBinaryMarkets({ venueId: "0x5bc0…", asset: "BTC", intervalSec: 900 });
await client.listLiveBinaryMarkets({ status: "Trading" }); // active only
(operatorId is the uint32 operator id; venueId is the venue's opaque
bytes32 hex id — enumerate the pairs in play with listBinaryVenueIds().)
For the underlying-asset universe and board size, use
listBinaryAssets() (the distinct assets with markets, e.g. ["BTC","ETH"])
and countBinaryMarkets({ operatorId?, venueId?, asset?, status?, phase? }) (the total
behind a filtered board, for pagination headers; phase splits live from past,
and omitting it counts both). The same filter carries resolvedSince and
voided, so countBinaryMarkets({ venueId, resolvedSince: now - 86_400 }) and
the same call with voided: true are a venue's settlement count and void count
over the trailing day — keyed on the terminal timestamp, so a void before
expiry counts too.
Each row carries marketAddress, poolAddress, operatorId, and venueId,
so a multi-venue UI can group the live board by origin without any
contract-level call — market discovery is the indexer's job, not the
MarketCreator's.
One correctness rule: gate writes on the selected market's live or on-chain
status, never indexer status. getMarketOnchain(marketId).status is a
market-scoped chain read; marketId is the selected market's bytes32 id. Use
the status from getLiveMarketByPool(pool) only after checking that the live
row's id matches the selected market id. The pool can host another market
after recycling, and indexed status lags.
An account's positions, complete and live
client.accountOutcomePositions gives one account's complete event-contract positions on
the client's chain: every YES and NO balance it holds, including markets it never traded
through your app. On an exchange, use exchange.engineFor(chainId).accountOutcomePositions.
const stop = client.accountOutcomePositions.subscribe(account, (view) => {
if (view.status === "ready") render(view.data.positions);
else if (view.status === "unavailable") renderUnknown(view.reason);
});
getSnapshot(account, { signal })reads the view once. Concurrent snapshot callers for one account share a read with independent cancellation; a ready live view is reused without a request.subscribe(account, onView)keeps it current and returns a function that stops it. Subscriptions to one account share one reader. After the last unsubscribe, it remains active for 30 seconds so a returning subscriber can reuse it. Successful reads poll the account's version every 5 seconds and reread the guarded snapshot when it changes. Recoverable failed reads retry after 5, 10, 20, 40, then 60 seconds, capped at 60 seconds. Anunavailablecapability is checked every 5 minutes over HTTP; this view has no capability subscription, so deployment recovery can take that long to be detected. Recovery restores the 5-second cadence. Held data's clock-derived market status is checked every 5 seconds during backoff.tooLargecontinues version polling without rereading unchanged oversized rows.stopLive()and owner cancellation end every reader immediately, including during reuse. Active subscribers receiveclosed, with no later view.stopLive()ends a snapshot read in flight withIndexerUnavailableError(live-reset); caller cancellation retains the caller's abort reason.- Each position has the market id, the side, the balance, the market's status and
claimable:unsettleduntil the market is finalized, thenknownwith the amount the balance redeems for, net of the settlement fee. A voided market pays its stored share for each side, which is not always a half. unavailablemeans unknown, not empty. Until the indexer enables this view on a chain, every read there isunavailable. Do not render it as "no positions".getPortfoliois a partial fallback: it returns at most 200 balances and does not say when it cut the list.tooLargemeans the account holds more positions than a view can prove complete, or its response exceeds the indexer's byte limit.- A read that fails throws from
getSnapshot. A live view reports it asfailed, or asstalewith the last complete data. A failed view'sretryingfield says whether another read is planned. A missing indexer endpoint is terminal (retrying: false); configure an endpoint on a new owner to recover. - Mark price and PnL are not in the view. Use
getOpenPositionsWithPnLfor those.
History + attribution (indexer)
Beyond the live board and portfolio, the indexer serves the fee, resolution, router, and vault-credit history the live tail doesn't materialize — all one-shot reads:
// How a market resolves: lifecycle events + the oracle reference link + the
// posted numeric oracle answer (joined by oracleQuestionId), in one round-trip.
const { events, reference, oracleAnswer } = await client.getMarketResolution(marketId);
// Router-level collateral flow for a wallet (redeem / mint / merge complete set),
// attributed to the TRUE end user even through the native/Permit2 periphery.
const actions = await client.getRouterActions(account, { market, limit: 50 });
// The per-fill fee streams behind getMarketFees' running total (all filter by
// `payer` — the order owner who funded the fee — plus recipient/market/pool).
const protoFees = await client.listProtocolFees({ market, payer });
const bldFees = await client.listBuilderFees({ builder });
const settleFees = await client.listSettlementFees({ market });
// Builder-approval directory (the indexed complement to the on-chain point read
// getBuilderApproval) — every user→builder cap, filterable by user or builder.
const approvals = await client.listBuilderApprovals({ user });
Vault credits. A payout that can't be delivered to the wallet is credited to
the owner's ERC20Vault balance instead. That history is append-only —
client.getVaultPayoutFallbacks(owner, { token }) lists the credit log for one
owner, and client.listVaultPayoutFallbacks({ pool, from, order: "asc" }) lists
it across every owner (an operator's alert or reconciliation reads it that way).
Each row carries the pool whose vault holds the credit — but the vault's own
credit/debit is silent (no event), so the live claimable balance is a chain
read: client.getVaultBalance({ vault: pool, owner, token })
(ERC20Vault.getWithdrawableBalance). Withdraw with
trader.withdrawVault({ vault, token, amount }).
Trading
const trader = client.createTrader({ privateKey }); // Node — or { walletClient } in the browser
const decimals = market.quoteDecimals; // indexed binary market's collateral scale
// Rest a limit order: buy 10 YES at 0.62.
const { orderId, fills, receipt } = await trader.placeOrder({
pool: market.poolAddress,
side: "BUY_YES",
price: probabilityToPrice(0.62, decimals), // raw collateral units per whole token
quantity: fromHuman(10, decimals), // raw outcome-token units
});
if (fills.length) console.log("crossed immediately:", fills);
else console.log("resting as order", orderId);
await trader.cancelOrder({ pool: market.poolAddress, orderId });
For a chain-only market read, use client.getMarketOnchain(marketId) and its
decimals and pool fields. A binding without indexerUrl has no indexed
quoteDecimals. You can omit indexerUrl for chain-only reads and writes;
indexed discovery and history need an indexer.
Networks and endpoints lists the
hosted indexers.
That read uses six decimals only if the token lacks the decimals() view;
other read failures throw. Verify a non-standard token's scale before writing.
The SDK helpers also default to six decimals if you omit their decimals argument.
Every write awaits its receipt — there is no bare hash to babysit, and
placeOrder resolves with the decoded orderId + fills from the same
round-trip. The escrow token (collateral for buys, YES/NO for sells) is
approved automatically on first use; pass autoApprove: false to manage
approvals yourself.
- Market orders are
orderType: ORDER_TYPE.MARKET(an IOC) placed at a crossing price — cross the best opposite level ± slippage so it sweeps and the remainder cancels.ORDER_TYPEalso hasFILL_OR_KILLandPOST_ONLY. - Sides and prices: all four
BinarySides take the YES-terms price. ABUY_NOat YES-price 0.62 escrowsquantity × (1 − 0.62)collateral. - Token wiring resolves from the pool contract automatically (cached); pass
outcomeToken/yesId/noId/collateralexplicitly to skip even that one-time read.
Stake-sized market orders ("bet $50 on Up")
A stake-first UI usually asks for a collateral stake, not a share
quantity. quoteBinaryStake is the inverse of quoteBinaryOrder: it walks the
live asks cheapest-first, sizing the largest quantity whose escrow at the worst
level touched stays within the stake — so the quoted shares and payout match
what actually fills, not a top-of-book estimate. The protective limit is padded
with a slippage cushion (default 3%, min 10 ticks) so the IOC still crosses a
moving book, aligned to the pool's on-chain tick grid, and the quantity is
lot-aligned with escrow re-fit under the stake — off-grid prices and non-lot
quantities are the pool's two rejection reasons, handled for you:
const quote = await client.quoteBinaryStake({
pool: market.poolAddress,
side: "BUY_YES", // Up; "BUY_NO" for Down
stake: fromHuman(50, market.quoteDecimals), // raw collateral — the max loss
});
if (quote) {
await trader.placeOrder({
pool: market.poolAddress,
side: quote.side,
price: quote.yesPrice, // protective limit, YES terms
quantity: quote.quantity, // shares = payout if this side wins
orderType: ORDER_TYPE.MARKET,
});
}
null means nothing is fillable (empty book, or a stake too small for one
lot) — disable the control rather than sending a doomed order. Unwind a
position with the sell-side sibling, which cushions a floor below the best bid
the same way:
const sell = await client.quoteBinarySell({
pool: market.poolAddress,
side: "SELL_YES",
quantity: position.balance, // lot-aligned down automatically
});
if (sell) {
await trader.placeOrder({
pool: market.poolAddress,
side: sell.side,
price: sell.yesPrice,
quantity: sell.quantity,
orderType: ORDER_TYPE.MARKET,
});
}
Unlike the buy side, a sell's quantity is your position, not a book-sized
fit — on a thin book the IOC fills what rests within the cushion and cancels
the rest. Compare sell.fillableQuantity (with sell.estProceeds, the
collateral it would raise) against sell.quantity and warn before submitting
when the unwind would be partial.
Both quotes read the live book (needs an active watch) plus the pool's tick/lot
grid via one cached eth_call (getBinaryBookParams). The pure kernels —
quoteBinaryStakeOverBook / quoteBinarySellOverBook / slippageForCrossing —
are exported for callers that already hold a book.
Builder / routing fees (optional)
A frontend that routes orders can attach itself to each order and charge a per-order fee — but only after the trader has opted it in on that pool:
await trader.approveBuilder({ pool, builder, maxFeeBpsTimes1k: 5_000n }); // allow up to 5 bps; 0 revokes
await trader.placeOrder({
pool,
side: "BUY_YES",
price,
quantity,
builder, // the routing/builder frontend
builderFeeBpsTimes1k: 5_000n, // per-order fee, pool bps×1000 unit
});
A non-zero builderFeeBpsTimes1k without a prior approval reverts
BuilderFeeExceedsCap on a BinaryPool (BuilderNotApproved on a SpotPool,
BuilderFeeExceedsApproval on a PerpPool). The enforced ceiling is the
trader's approval clamped by the pool's protocol-wide cap — read it with
trader.getEffectiveBuilderApproval({ pool, user, builder }) (the pool-wide
cap alone is trader.getMaxBuilderFeeBpsTimes1k(pool)). On the
exchange surface the same rides createOrder's params.builder /
params.builderFeeBpsTimes1k, on binary, spot and perp alike — each pool holds
its own approval, so opt in once per pool.
Complete sets and settlement
const amount = fromHuman(10, market.quoteDecimals); // raw collateral and outcome units
await trader.mintSet({ pool, amount }); // collateral → equal YES + NO
await trader.burnSet({ pool, amount }); // YES + NO → collateral back
await trader.redeem({ market: marketAddress, amount }); // winning token → collateral (post-resolution)
// A VOIDED market has no winner — both legs pay — so name the leg yourself.
// One entry per leg claims both in a single transaction:
await trader.redeemMany({
entries: [
{ marketId, outcomeIdx: 0, amount: yesAmount },
{ marketId, outcomeIdx: 1, amount: noAmount },
],
});
mintSet/burnSet are how you take (or unwind) a both-sides position without
touching the book; redeem pays out after resolution (it looks up the winning
outcome on-chain if you don't pass it). That lookup is resolution-only: a
voided market has no winning outcome, so redeem throws unless you pass
outcomeIdx — it will not guess a leg for you, because on a void both legs are
redeemable and only you know which you hold. An unresolved market also throws;
the SDK reads the payout vector only after confirming resolution. If the venue
configured a settlement fee, the pool skims it ONCE from the whole winning backing at the
first winning redeem (SettlementFeeCharged) and every winner redeems for
1 − fee — voided markets refund both sides with no fee. Demo-stack extras:
faucet() mints the test collateral; resolve/voidMarket drive the
FakeOracle.
Unified exchange redemption
The exchange wrapper takes display units rather than raw token amounts. A
settled market is no longer in fetchMarkets() — a series market resolves,
finalizes and hands its pool to the next market — but fetchBalance() still
reports a holding on it under the market's own symbol, and that symbol still
resolves for redeem. Every pool-routed verb (createOrder, mintSet,
burnSet, the book, tape and ticker reads and watches) refuses a settled
market, because its pool now serves the successor.
createOrder, mintSet and burnSet also read the pool's current market
contract before funding or signing. A cached Trading row does not authorize
a write after its pool has moved to a successor. An observed mismatch throws
InvalidInputError; refresh with loadMarkets(true) and select the intended
market again. A failed check throws its RpcError or ContractRevertError
and sends nothing. The check uses one uncached pool read on the existing owner
connection and needs no module address. Recycling after this preflight read
can still race transaction execution. Historical redeem keeps using the
original market id.
On a resolved market, redeem selects the indexed winner:
const bal = await exchange.fetchBalance();
const winningAmount = bal[`${symbol}#YES`]?.total ?? 0;
if (winningAmount > 0) await exchange.redeem(symbol, winningAmount);
A voided market has no single winner. Pass the leg you hold and call once for each leg you want to redeem:
await exchange.redeem(symbol, yesAmount, { outcomeIdx: 0 });
The SDK rejects a malformed or impossible final payout vector. Only an absent vector on a legacy market uses the compatibility fallback.
A maker loop, end to end
const watch = await client.watchMarket(pool);
const trader = client.createTrader({ privateKey });
client.subscribeLive(async () => {
const book = client.getLiveBinaryOrderBook(pool, { depth: 1 }); // zero RTT
const mine = client.getLiveUserOrders(pool, me, { limit: 50 }).filter((o) => o.status === "Open"); // zero RTT
// decide → place/cancel; each write resolves after its receipt arrives
});
The live store is the only state a quoting loop needs: the book, your working orders, and fills all update the moment the chain emits them.
Binary claimable reads inspect indexed outcome positions. getClaimable(account)
returns a complete array only when the account has at most 200 nonzero positions.
It throws IncompletePortfolioError for a larger wallet. Use
client.getClaimablePage(account, { limit: 200, offset: 0 }) for bounded pages,
and follow nextOffset even when claims is empty. Losing and open positions
still count toward that offset. Each page detects continuation with one extra
position. Balance changes between pages can move rows; refresh indexed estimates
and confirm chain balances before redeeming.
getPortfolio accepts positionsLimit and positionsOffset for the same indexed
position pages. Check positionsTruncated and follow positionsNextOffset.
Other derived views that consume one portfolio page, including fetchBalance
and getOpenPositionsWithPnL, still cover only that page's binary positions.