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
| Contract | Guarantee |
|---|---|
| Parent action | One signed parent action controls all legs and consumes one action nonce. |
| Scope | Explicit legs only. It does not implicitly replace an entire market, book, side, or all quote-managed orders. |
| Leg order | Legs are applied in array order. For a leg with cancel_order_id, cancel is evaluated before that leg's replacement placement. |
| Atomicity | All-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 identity | Nonzero-quantity replacement legs derive deterministic child order ids from the signed parent payload and leg index. |
| Admission | X-BSL-Result-Mode: ack confirms the configured admission boundary only. |
| Terminal truth | Final 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
| Field | Replaceable | Notes |
|---|---|---|
| Price | Yes | Must satisfy tick size |
| Quantity | Yes | Must satisfy lot size and collateral checks |
| Time-in-force | Limited | Some transitions require cancel and new order |
| Side | No | Use cancel and new order |
| Market | No | Use cancel and new order |
| Account | No | Use 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 failure | Engine text in receipt detail | actionResults[].rejectCode |
|---|---|---|
| Price or quantity zero | order price must be greater than zero / order quantity must be greater than zero | ENGINE_REJECTED |
| Outcome price above 1.000 (limit legs) | price above binary cap: PriceMicros(N) | ENGINE_REJECTED |
| Tick or lot misalignment | price PriceMicros(N) is not aligned to tick PriceMicros(T) / qty Qty(N) is not aligned to lot Qty(L) | ENGINE_REJECTED |
Quantity above maxOrderQtyMicro | qty Qty(N) exceeds max Qty(M) | ENGINE_REJECTED |
| Post-only leg would cross | post-only order would cross the book | POST_ONLY_WOULD_CROSS |
| Not enough USDC or YES/NO shares (outcome) | insufficient USDC balance, RiskWouldExceedAvailableUsdc: …, InsufficientShares: …, insufficient YES balance / insufficient NO balance | INSUFFICIENT_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 expiryTsMs | market MarketId(N) expired for trading at T (normally caught earlier at ingress, see below) | ENGINE_REJECTED |
| Cancel target owned by another account | unauthorized order cancel | ENGINE_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
QuoteReplaceorSpotQuoteReplace. - Legacy HTTP uses the compatibility cancel-replace endpoint in the API reference.