Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Managing Orders

Once an order is placed, a trader can read it back, amend it in place, cancel it, fire it themselves if it is a trigger order, and collect what the pair owes them. Each of these is a builder on bz.trade that returns a PreparedCall.

The samples assume bz connected as in Connecting with a price source, user the wallet’s address, eth a market, and a run(call) helper that simulates, sends and waits as on Sending Transactions. The protocol’s rules are in Orders (Amending, Cancellation & cleanup).

Reading an Account’s Orders

bz.account.accountSnapshot(market, user) returns the account on one market, including every live order, decoded (Accounts & Portfolios has the whole snapshot):

const snap = await bz.account.accountSnapshot(eth, user);
for (const o of snap.orders) {
  console.log(
    `#${o.orderId} ${o.typeName} ${o.isLong ? "buy" : "sell"} ${formatUnits18(o.remaining)} of ${formatUnits18(o.size)}`,
    o.inBook ? `resting at ${formatUnits18(o.limitPrice)}` : `waiting for ${formatUnits18(o.triggerPrice)}`,
    o.neverExpires ? "" : `expires in ${o.blocksLeft} blocks`,
    o.bounty > 0n ? `bounty $${formatUnits18(o.bounty)}` : "",
  );
}
snap.takeProfit; // the position's attached TakeProfit, or null
snap.stopLoss;
snap.claimableFees; // refunded bounties, keeper rewards, relayer and integrator fees on this pair

snap.orders lists the live orders in the account’s active set (Limit GTC and StopLimit), then the attached TakeProfit and StopLoss. Each is an AccountOrder:

FieldMeaning
orderIdThe order’s id on the pair (bigint).
orderType, typeName0 to 4, and "Market", "Limit", "StopLimit", "TakeProfit" or "StopLoss".
isLong, isPostOnly, tifAs created (tif: 0 GTC, 1 IOC, 2 FOK).
size, filled, remainingTotal size, the part filled, and what is left (for a TP/SL, capped at the position it closes).
limitPrice, triggerPriceThe limit, and the trigger price for trigger types (0 for the others).
bountyWhat the keeper bounty escrow still holds for the order.
maxSlippageBpAs created.
expiryBlock, neverExpires, blocksLeftThe expiry block, whether it never expires (TP/SL), and blocks until it expires (null when it never does).
inBookResting in the book: a Limit, or a StopLimit that has armed. A trigger order still waiting is false.
closesWholePositionA TP/SL sized to close whatever the position is when it fires.
attached"takeProfit" or "stopLoss" for the position’s attached orders, else null.
integratorThe integrator the order names.
recordThe order as the pair stores it (pair.orders(id), decoded).

Other reads:

  • bz.orders.readOrderState(pair, user): just the position and the order slots (the live Limit + StopLimit ids and the attached TP and SL ids), in one batched read. The order form’s checks use it (Placing Orders).
  • bz.orders.readTriggerBounty(pair, orderId): what one order’s bounty escrow holds.
  • bz.live.watchAccount(user, markets, onUpdate): a snapshot after every event that touches the account, plus the events themselves (OrderUpdated, OrderAmended, OrderAmendSkipped, Trade, …). See Live Updates.
  • bz.history.orders({ account }) and bz.history.orderEvents(...): past orders and their lifecycle from the subgraph. See History.

Amending

bz.trade.amendOrders(pair, from, amends) changes resting Limit (GTC) orders in place: each gets a new price and a new total size, and keeps its id, expiry and integrator.

// Reprice order #42 to 2990 and make its total size 1.5 (filled part included).
await run(await bz.trade.amendOrders(eth.address, user, [{ orderId: 42n, limitPrice: parseUnits18("2990"), size: parseUnits18("1.5") }]));

Each amendment is an AmendInput:

FieldMeaning
orderIdOne of from’s resting Limit GTC orders.
limitPriceThe new limit price, BAZAAR precision.
sizeThe new total size, the filled part included: it must exceed what has filled.
predOptional: the order to rest directly behind at the new price (0 = none). Found for you when both hints are omitted.
pred2Optional: a second candidate, tried when the first has left the book.
bestEffortSkip this amendment instead of reverting the whole batch when the order has filled, been canceled or expired, the size is at or below what filled or leaves under $5, or the new price would cross. Default false.

How an amendment moves the order: a new price, or a larger size, sends it to the back of its new price level; a smaller size at the same price keeps its place. Up to 32 amendments per call (MAX_AMENDS_PER_CALL, the same as the active-order cap, so one call can requote every order). The same order may appear more than once; each amendment applies in turn. Margin is judged once, on the set of orders the batch leaves. A batch is all or nothing, except for best-effort amendments, which the pair skips and reports with OrderAmendSkipped (reason Inactive, Expired, SizeFilled, BelowMinimum or WouldCross).

// Requote every resting limit order 0.2% higher in one call. bestEffort: an order that filled or
// expired meanwhile is skipped (OrderAmendSkipped) instead of reverting the whole batch.
const resting = snap.orders.filter((o) => o.typeName === "Limit" && o.inBook);
// Moving up: amend the asks first, so a bid's new price is not judged against an ask about to move.
resting.sort((a, b) => Number(a.isLong) - Number(b.isLong));
const amends = resting.map((o) => ({
  orderId: o.orderId,
  limitPrice: (o.limitPrice * 10_020n) / 10_000n,
  size: o.size, // unchanged size and a new price: the order goes to the back of its new level
  bestEffort: true,
}));
if (amends.length > 0) await run(await bz.trade.amendOrders(eth.address, user, amends));

What the builder checks first, as the pair’s _amendOne will, throwing InputError:

  • the order exists, is from’s, and is a Limit GTC (“only those can be amended. Cancel it and place a new one.”);
  • it is still open and unexpired (not raised for a best-effort amendment);
  • the new price is within ±50% of the oracle price;
  • the new size exceeds what has filled, and what is left to fill is worth at least $5 (or is the whole position) (not raised for best effort);
  • the new price does not cross the best order on the other side (“Place a new order to trade now.”), unless that order is itself moved earlier in the same batch (not raised for best effort);
  • a move (new price or larger size) needs about a minute of life left (the minimum lifetime plus the SDK’s 40-block margin): “expires too soon to be moved. Cancel it and place a new one.”

Amendments are checked in order, each against the order as the earlier ones left it, so order them so no step crosses: when requoting upward, move the asks before the bids, as the sample does.

Hints are part of the amendment. Unlike an order’s MatchHints, an amendment’s hints are packed into its id word (orders.amendIdWord(orderId, pred, pred2, bestEffort): the id in bits 0 to 63, the hints in bits 64 to 191, the best-effort flag in bit 192), and the flat word array is what a gasless AmendOrders request signs. For a gasless amendment the hints are fixed when the user signs; a relayer cannot refresh them. orders.encodeAmends(amends) and orders.decodeAmends(words) convert between AmendInput[] and the flat array (one triple of id word, price and size per amendment).

amendOrders always carries a price update (the placement band and margin are judged at a fresh price), so it needs a price source and should be sent promptly.

bz.trade.prepareAmends(pair, owner, amends) runs the same checks and fills in hints without building a call:

// The same checks and hints without building a call (a gasless amendment signs prep.words).
try {
  const prep = await bz.trade.prepareAmends(eth.address, user, [{ orderId: 42n, limitPrice: parseUnits18("2990"), size: parseUnits18("1") }]);
  console.log(prep.amends[0]!.pred, prep.words.length); // the hint found, and 3 words per amendment
} catch (e) {
  if (e instanceof InputError) show(e.message); // "orderId: Order #42 is no longer open."
}

It returns { amends, words, price, currentBlock }: the amendments with hints, the flat words, and the price and block they were checked at.

Canceling

bz.trade.cancelOrders(pair, from, orderIds) cancels up to 200 of from’s orders in one call (MAX_CANCELS_PER_CALL). Each is unlinked from the book in the same transaction, and any keeper bounty it still holds returns to from’s claimable balance. There is no margin check and no price update.

await run(await bz.trade.cancelOrders(eth.address, user, [42n, 43n]));

// Everything at once: every live order, up to 200 per call.
const ids = snap.orders.map((o) => o.orderId);
if (ids.length > 0) await run(await bz.trade.cancelOrders(eth.address, user, ids.slice(0, 200)));

The builder refuses the following (trade.checkCancelIds(ids) runs the list checks alone, with no reads):

  • an empty list, more than 200 ids, a duplicate, or a non-positive id;
  • an order that does not exist or is not from’s;
  • an order that has already ended (filled, expired, auto-canceled) and holds no bounty: “Order #12 is already closed.” or “… has already expired.”.

An order that has ended but still holds a bounty in escrow can be “canceled” to reclaim it; the summary then says so (Cancel order #12 (reclaiming its bounty)).

Firing Your Own Trigger Orders

A trigger order with no bounty attracts no keeper, so its owner fires it. bz.trade.executeTriggeredOrders(pair, from, orderIds) executes up to 20 of from’s own StopLimit, TakeProfit and StopLoss orders (MAX_TRIGGER_EXECS_PER_CALL) against a fresh price:

// Fire this user's own trigger orders (the zero-bounty ones no keeper will) that the live price has reached.
const { price } = await bz.trade.livePairPrice(eth.address);
const due = snap.orders
  .filter((o) => !o.inBook && o.triggerPrice > 0n && triggerReached(o.orderType, o.isLong, o.triggerPrice, price.spot))
  .map((o) => o.orderId)
  .slice(0, 20);
if (due.length > 0) await run(await bz.trade.executeTriggeredOrders(eth.address, user, due));

Each order must be from’s, a trigger type, live, and still waiting outside the book (a StopLimit that has already armed is resting and refused). The builder reads the price the call will carry and refuses an order whose trigger it has not reached (“Order #7’s trigger (2700) has not been reached; the price is 2750.”); triggerReached(orderType, isLong, triggerPrice, price) is the same test, exported for a UI. A StopLimit gets a hint for the slot it will rest in. On chain the pair evaluates each trigger against the price the call carries and skips, without reverting, any that is not eligible then. An execution of your own order pays its bounty share to you.

Watching for the moment a trigger is reached is a job for live prices: subscribe to the market (Live Prices) and build the call when triggerReached turns true.

Claiming Fees

bz.trade.claimFees(pair, from) pays from everything the pair owes it: refunded trigger bounties, keeper rewards, relayer fees and integrator fees. It works in any pair state and has no gasless form: the caller pays the gas.

if (snap.claimableFees > 0n) {
  const claim = await bz.trade.claimFees(eth.address, user); // "Claim 1.25 USDC"
  await run(claim);
}

The builder reads the claimable amount from the lens first and refuses when it is zero (InputError on pair: “You have nothing to claim on this market.”); the summary names the amount. Balances are per pair: snap.claimableFees for one market, bz.account.portfolio(user) totals them across markets. CollateralLib__ClaimTransferFailed means USDC refused the transfer (the address cannot receive USDC).

What Can Go Wrong

You seeIt means
InputError on orderId: “Order #42 is not a resting limit order …”Only Limit GTC orders can be amended. Cancel and place a new one.
InputError on limitPrice: “… would cross the book (best ask 3001.5). Place a new order to trade now.”An amendment cannot take liquidity.
InputError on orderId: “Order #42 expires too soon to be moved.”Less than about a minute of life left.
InputError on amends: “At most 32 orders can be amended at once.”Split the batch.
InputError on orderIds: “Order #12 is already closed.”Nothing left to cancel or reclaim.
InputError on orderIds: “Order #7’s trigger … has not been reached”The price has not met the trigger yet.
OrderManagementLib__... from the simulationThe pair’s own refusal; the message says what to change.