Skip to main content

BSL Cancel-Replace Pattern

Cancel-replace lets a liquidity provider or HFT client amend price, quantity, or order flags without manually issuing separate cancel and place calls.

Why it matters​

Separate cancel and place operations can expose a strategy to stale quote risk or temporary loss of queue position. Cancel-replace gives the client one operation with one acknowledgement path.

Preferred path​

For new integrations, use a signed QuoteReplace or SpotQuoteReplace action inside an order-entry batch:

{
"version": 1,
"actions": [
{
"payload": {
"account": "0x1111111111111111111111111111111111111111",
"nonce": 42,
"ts": 1781190000000,
"action": {
"SpotQuoteReplace": {
"market": 1,
"legs": [
{
"cancel_order_id": "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"side": "Bid",
"price": 999900,
"qty": 100000,
"time_in_force": "post_only"
}
]
}
}
},
"signature": {
"scheme": "EcdsaSecp256k1",
"bytes": [1, 2, 3]
}
}
],
"idempotencyKey": "replace-bid-1"
}

In current private beta, submit it through POST /api/order-entry/binary or the direct compatibility route POST /api/v1/mm/orders/batch.bin with X-BSL-Result-Mode: ack and X-Senticore-Response-Mode: detailed. The canonical POST /api/v1/bsl/orders/compact route uses the same signed compact payload and requires institutional_agent HMAC auth or a provisioned lane key.

For QuoteReplace, a leg with qty: 0 is cancel-only. A leg with nonzero qty places a deterministic child order id derived from the signed parent payload and leg index.

QuoteReplace guarantees​

ContractGuarantee
Parent actionOne signed parent action controls all legs and consumes one action nonce.
ScopeExplicit legs only. It does not implicitly replace an entire market, book, side, or all quote-managed orders.
Leg orderLegs are applied in array order. For a leg with cancel_order_id, cancel is evaluated before that leg's replacement placement.
AtomicityAll-or-nothing per action. If any leg is rejected, the whole action is rolled back: cancels already applied by earlier legs are undone, no leg stays placed, and the action nonce is not consumed. See Failure behavior.
Child identityNonzero-quantity replacement legs derive deterministic child order ids from the signed parent payload and leg index.
AdmissionX-BSL-Result-Mode: ack confirms the configured admission boundary only.
Terminal truthFinal open, filled, canceled, or rejected state comes from receipts, private stream, FIX/BSL drop-copy, or account order reads.

Do not treat HTTP 200 as proof that every replacement order is resting. A risk reject, market halt, post-only cross, stale cancel target, or terminal engine reject can still appear after admission.

If local order-id tracking is incomplete, do not send a wide QuoteReplace range and assume older quotes are removed. First reconcile open orders from private state or run the account-safe cancel-all flow scoped by market/book/side, then seed a new quote family and persist every returned derivedOrderIds[]. The BSL response includes quoteFamilyId on echoed quote-replace legs and aggregated quoteFamilyScopes[] in bsl.reconciliation.quoteReplaceSummary. Those IDs are deterministic for account + market + book + side and are meant for reconciliation and scoped cancel-all recovery; they are not persisted engine-side order tags.

Scoped cancel-all recovery​

When a quote engine loses its local order-id map, use the BSL recovery endpoint before publishing a fresh quote family:

POST /api/v1/bsl/orders/cancel-all
Content-Type: application/json
SC-Auth-Version: 2
SC-Key: <institutional_agent key>
SC-Nonce: <fresh tuple nonce>
SC-Signature: <hmac signature>
{
"account": "0x2222222222222222222222222222222222222222",
"market": 1,
"book": "YES",
"side": "Bid"
}

account is the 20-byte engine account. With account-bound institutional_agent auth it may be omitted; otherwise send it explicitly. market, book, and side are optional filters. Omitting all three is a whole-account panic cancel and should be reserved for emergency recovery.

The response is an acknowledgement, not terminal truth:

{
"ok": true,
"accepted": true,
"cancelledOrders": 1,
"operation": "cancel_all_by_market_book_side",
"ackIsTerminalState": false,
"reconciliation": {
"terminalTruth": "private_stream_dropcopy_receipts_or_account_reads",
"nextAction": "wait_for_cancel_reports_then_seed_fresh_quote_family"
}
}

After the ACK, wait for private stream, FIX/BSL drop-copy, receipts, or account order reads to confirm terminal cancel state before assuming the book is clean.

Legacy facade​

The legacy-compatible HTTP facade remains available:

POST /api/v1/mm/orders/replace
POST /api/v1/mm/orders/cancel-replace

It accepts a cancelOrderId, replacement order, optional signatures, mode, time-in-force, STP, and advanced flags. It exists for clients already wired to the earlier compatibility facade. New quote engines should use signed BSL action batches.

Replaceable fields​

FieldReplaceableNotes
PriceYesMust satisfy tick size
QuantityYesMust satisfy lot size and collateral checks
Time-in-forceLimitedSome transitions require cancel and new order
SideNoUse cancel and new order
MarketNoUse cancel and new order
AccountNoUse cancel and new order

Failure behavior​

A QuoteReplace or SpotQuoteReplace is one action. The engine applies its legs one after another and keeps a rollback record; if any leg fails, it restores the ledger, the market's book and the nonce to their state before the action. There is no partial result in which one leg rests and another does not.

Every one of these per-leg failures rejects the whole action, not only the failing leg. The second column is the engine text that appears in receipts[].detail of GET /api/v1/bsl/receipts (the lane may put a prefix such as engine apply failed: in front of it, so match on a substring). The third column is the coarse rejectCode of the same action in actionResults[] (full response mode, drop-copy); there rejectReason is a fixed public sentence, not the engine text.

Leg failureEngine text in receipt detailactionResults[].rejectCode
Price or quantity zeroorder price must be greater than zero / order quantity must be greater than zeroENGINE_REJECTED
Outcome price above 1.000 (limit legs)price above binary cap: PriceMicros(N)ENGINE_REJECTED
Tick or lot misalignmentprice PriceMicros(N) is not aligned to tick PriceMicros(T) / qty Qty(N) is not aligned to lot Qty(L)ENGINE_REJECTED
Quantity above maxOrderQtyMicroqty Qty(N) exceeds max Qty(M)ENGINE_REJECTED
Post-only leg would crosspost-only order would cross the bookPOST_ONLY_WOULD_CROSS
Not enough USDC or YES/NO shares (outcome)insufficient USDC balance, RiskWouldExceedAvailableUsdc: …, InsufficientShares: …, insufficient YES balance / insufficient NO balanceINSUFFICIENT_BALANCE (some risk texts: ENGINE_REJECTED)
Not enough USDC or base asset (spot)RiskWouldExceedAvailableUsdc: … / InsufficientSpotAsset: …RISK_WOULD_EXCEED_AVAILABLE_USDC for a bid, INSUFFICIENT_SPOT_ASSET for an ask
Market past expiryTsMsmarket MarketId(N) expired for trading at T (normally caught earlier at ingress, see below)ENGINE_REJECTED
Cancel target owned by another accountunauthorized order cancelENGINE_REJECTED

ENGINE_REJECTED is a catch-all. The price cap, tick/lot, max-quantity and expiry failures have no dedicated machine code yet; until one is assigned, classify them from the receipt detail with the substrings above and treat them as non-retriable until the order is corrected.

A leg on a market that is not open, or an action whose ts is at or after expiryTsMs, is refused earlier, at ingress: the whole HTTP request fails with acceptedActions: 0 and error: "market N lifecycle=S is not accepting new orders" or "market N expired for trading at T", and nothing is sequenced. See BSL rejects.

A cancel target that is already terminal (filled or cancelled) is skipped and does not fail the action. A cancel target the engine does not know at all is skipped on the JSON signed-action path, but on the compact binary path it fails the whole action with order not found. Persist derivedOrderIds[] so every cancel_order_id you send refers to an order the venue has seen.

The reject is reported for the parent action, not per leg, and does not say which leg failed. In actionResults[] the action carries status: "rejected" and nonceConsumed: false. On the compact lane a replace is sequenced as a group of cancel and place frames, one sequence number each; every one of them shows status: "dropped" in the receipts with the same detail. Because the nonce is not consumed, you may re-sign the corrected action with the same nonce or use another unused in-window nonce. After a rejected replace, the orders you meant to cancel are still open. Reconcile final order state before updating local strategy state.

To keep one bad leg from taking down a whole quote refresh, check each leg against the market rules (price in (0, 1000000] on outcome books, tick, lot, maxOrderQtyMicro, a passive price for post-only legs, free inventory for asks) before signing, and consider separate actions for bids and asks when inventory is uncertain.

FIX and binary equivalents​

  • FIX uses OrderCancelReplaceRequest (35=G).
  • BSL compact order entry uses QuoteReplace or SpotQuoteReplace.
  • Legacy HTTP uses the compatibility cancel-replace endpoint in the API reference.