How to detect when an order fills
This guide shows you how to learn that a resting order was filled, partially filled, or cancelled, with the least latency and without polling. It assumes a configured SomniaMarkets instance with a signer, and a market watch opened before the order was placed.
Read the placement result first
A createOrder that crosses the book fills in the same transaction. Its result already says so.
const order = await exchange.createOrder("SOMI/USDso", "limit", "buy", 10, 0.116);
if (order.status === "closed") {
// fully filled in the placement transaction
} else if (order.status === "open") {
// resting: order.filled may still be > 0 for a partial fill
}
status is "closed" when remaining is zero, "open" when a remainder rests, and "canceled" when an immediate-or-cancel or market order left an unfillable remainder. The raw fills are in order.info.fills.
Wait for the status to flip
watchOrders(symbol) returns your orders on the market from the live store. The first call returns the current list; each later call resolves when the list changes. Loop until your order is no longer open.
let mine = order;
while (mine.status === "open") {
const orders = await exchange.watchOrders("SOMI/USDso");
mine = orders.find((o) => o.id === order.id) ?? mine;
}
console.log(mine.status, "filled", mine.filled, "of", mine.amount);
The list changes on every order event for your account on that market, including partial fills, so the loop also observes filled growing while status stays "open".
Open the market watch before placing. watchOrderBook, watchTrades, or watchOrders on the symbol all open it. The watch learns about an order placed after it opened from the chain event, in the block the order lands. It learns about an order placed before it opened from the indexer snapshot, which lags the chain by the indexing delay, typically a few seconds.
Watch your trades
watchMyTrades(symbol) returns your fills on the market, newest first, and resolves on each new one. Use it when you care about executions rather than order state, for example to update inventory.
let seen = new Set<string>();
while (true) {
const trades = await exchange.watchMyTrades("SOMI/USDso", 20);
for (const t of trades) {
if (seen.has(t.id)) continue;
seen.add(t.id);
console.log(t.datetime, t.side, t.amount, "@", t.price, "cost", t.cost);
}
}
On spot and perp markets side is absent: the pools do not attribute a fill to a side in a way the SDK can map to you. Read t.info.takerIsBid and compare t.info.maker with your address instead. On binary markets side is your own side when the fill carries one.
Use the engine for raw values
exchange.client.getLiveUserOrders(pool, owner) and getLiveUserFills(pool, owner) are the synchronous views behind the two verbs. They return LiveOrder and LiveFill rows with exact raw values as decimal strings; wrap a value in BigInt() to compute with it. The API reference lists the fields. Subscribe to changes with client.subscribeLive(listener).
In React, useLiveUserOrders and useLiveUserFills re-render on the same changes. See Use the React hooks.
Read your own writes at chain head
When a write threw RpcError after sending, or when no watch is open, ask the pool directly. These reads answer from the contract in one round-trip and know only what is open now.
const t = exchange.market("SOMI/USDso");
const owner = exchange.walletAddress;
if (!owner) throw new Error("configure a signer before reading your open orders");
const openIds = await exchange.client.getOwnOpenOrdersOnchain(t.pool, owner);
const stillOpen = openIds.some((id) => id.toString() === order.id);
getOrderOnchain(pool, orderId) returns one order's on-chain state. For history, including filled and cancelled orders, use the indexer reads getOpenOrders, getOrders, and getOrderFills; they lag the chain by the indexing delay.
Read a maker order a fill already removed
Decoding OrderFilled yourself gives you the fill's fillPrice, quantityFilled and makerRemainingQuantity, but names the maker only by id - and a fill removes the maker order in the same transaction, so at head it is gone. Pin the read one block earlier to recover the side the fill took:
const maker = await exchange.client.getOrderOnchain(pool, makerOrderId, { blockNumber: fillBlock - 1n });
// `null` is an UNRESOLVED side, not a sell: an order placed and filled inside one
// block has no state one block earlier, and its side is in that block's OrderPlaced.
const takerBought = maker === null ? null : !maker.isBid;
What it answers, and what it does not:
- The maker's identity -
isBid,owner,userData,expireTimestampNs- cannot change under a given id, so the pinned read is exact for it, and no fill log carries it:OrderFillednames the maker by id alone.OrderPlacedcarries it, but only a consumer that was already listening when the maker rested has that log, which is the case this read exists for. - Not the price. It is immutable too, but a fill executes at the maker's resting price and the book emits exactly that, so
OrderFilled.fillPricealready is the maker's price. Reading for it is redundant work. - Both quantities are the values at the block boundary, not at your fill.
quantityRemainingmoves on every fill, andreduceOrderdecrementsfullQuantityandquantityRemainingtogether under the same id - so an earlier transaction in the fill's own block makes either value stale for your fill, and no block-level read can see between transactions. TakequantityFilledandmakerRemainingQuantityfrom the event instead.
Two edges answer plausibly rather than failing:
- A partial fill leaves the maker order in place with a smaller
quantityRemaining, so reading at the fill's own block succeeds with different numbers instead of returningnull. - An order placed and filled inside one block does not exist at
fillBlock - 1n. That read isnull, and the placement is in the same block'sOrderPlaced.
A recent block answers against a full node. An older one needs archive state, so this suits a live tape rather than a backfill.
Choose by need
| Need | Use | Latency |
|---|---|---|
| Did the placement itself fill | createOrder result | none |
| Did a resting order change | watchOrders | next block |
| What executed, for inventory | watchMyTrades | next block |
| Is it still open, after a transport error | getOwnOpenOrdersOnchain | one round-trip |
| Full history for a report | getOrders, getOrderFills | indexing delay |