Placing orders
How matching works: frequent batch auctions
MNX does not match orders the instant they arrive. Orders are collected into short batches and matched at discrete intervals (about every 200ms) — a Frequent Batch Auction (FBA). Each batch clears at a single uniform clearing price: the price that maximizes matched volume, applied to every trade in the batch.
When one side of a batch has more volume than can trade, better prices fill first: liquidation orders (the reduce-only market orders MNX submits to close under-margined positions) first of all, then other market orders, then limit orders from the best price level down. The last price level that trades is shared pro rata (in proportion to each order's remaining size) rather than by queue position, and the lots left over after rounding go to the orders whose shares were rounded down the most. Every fill still pays the one clearing price.
When several prices would match the same volume, the batch clears where a continuous order book would: at the price the resting orders alone fill the trade at. A market sell clears at the lowest bid level the resting bids need to fill it, a market buy at the highest ask level, and a new limit order that crosses resting orders at their price. Only orders that have rested in the book for at least 400ms count toward that price, longer while a batch is settling. Otherwise, for example when the resting orders alone cannot fill the trade, market orders arrive on both sides, an account's own orders would trade with each other, or a fill at that price would fail a margin check, the batch clears at the oracle price rounded to the market's tick if the oracle lies between those prices, and at the one nearest the oracle if not, so the level being shared can sit slightly above the clearing price for buyers, or below it for sellers.
Practical consequences for an API client:
- A successful
POST /v0/ordersmeans your order was validated and accepted into the book — not that it traded. Matching happens in a subsequent batch. - Market orders are immediate-or-cancel: whatever matches in the next batch fills, and the rest is cancelled. If that batch is aborted before it settles, the market order is usually cancelled with reason
batch_aborted. It gets one more try in the market's next batch instead when the aborted batch was never sent, or when the failure was blamed on another account, provided the abort comes within 5 seconds of the batch being sealed and trading is not paused. On that try it trades like any market order, so an unfilled rest is cancelled with the usual reason, such asmarket_order_iocormarket_order_no_liquidity. It getsbatch_abortedonly if no batch takes it in time or the batch that does fails too. A market order the abort blames for the failure is cancelled with a reason naming the failure. Wait for a market order's own fill or cancellation before replacing it: resubmitting as soon as its batch aborts can fill twice. A triggered take-profit or stop-loss market order, or a triggered reduce-onlySTOP_MARKETorder, is the exception: a batch that fills none of it puts it back to waiting for its trigger instead of cancelling it, and an aborted batch returns it to the book (see Order types). - All fills in one batch settle at the same clearing price, which may be better than your limit price.
- Sub-batch-interval speed buys little; there is no race to the top of a queue within a batch. Placement order still breaks exact ties between leftover lots at the shared price level, and decides which orders are left out when a batch reaches an internal fill limit.
- Within your own account, reduce-only orders (triggered take-profit and stop-loss orders included) fill after your ordinary orders on the same side, and only for the position those leave. A take-profit and an ordinary sell for the same position cannot together reverse it.
- While MNX is liquidating your position, a batch that holds the liquidation order leaves out your other orders on its side: limit orders keep resting, and market orders are cancelled with the reason
liquidation_preflight.
Opening auctions
Some new markets with no outside price, such as a numeric future priced from its own order book, open with an opening auction, much as stock exchanges open each day. The market lists at a price the exchange chooses, takes orders for about a day, then opens at the one price those orders set. Its config reports it: auction_end is the close, auction_rules holds the listing price (reference_price_e18_raw) and the auction's limits, and opened_at is set once it has opened.
- Before the close the market takes limit orders only, priced inside a collar around the listing price (the book's
auction.collar, about 25% below to 33% above it). Market, stop, take-profit and stop-loss orders are refused with409 pre_open, and a price outside the collar with409 auction_price_outside_collar. Leverage is capped at the auction'sauction_rules.max_leverage, and an order must not expire before 24 hours after the close. An order that would cross one of your own resting orders is refused, and so is any order once the auction holds 2,000 with409 auction_book_full. A numeric future's sell reserves margin as if it filled at the top of the collar, since the opening price can be anywhere up to it; the excess is released when it fills, is cancelled or the market opens. The book is sealed: the public book shows no levels and no prices, its WebSocket topic sends no level deltas, and the market's price does not move with its orders. Order counts stay public. You can cancel your orders. - At the close the exchange sets the auction's result: the single price that fills the most, the matched quantity and the imbalance, published in
auction.resultwithout the book. If an account with orders that could trade has a deposit, withdrawal or margin change still settling, the close waits up to 30 seconds for it, or up to five minutes when those orders are the only ones that could trade. New orders are refused with409 auction_closed, and so are cancels through the API until the market opens or 24 hours have passed. You can still cancel an order on chain. - The open. The market's oracle then moves to the auction's price in steps of at most 2.5% (about 9 minutes from the edge of the collar), shown as
auction.walk_to_e18_raw, and the opening batch fills every order that crosses that price, at exactly that price. Every order in the opening batch trades at the same price, which can be well inside your limit, and there is no advantage in placing early. When one side offers more than trades, its orders priced better than the auction's price fill first, and the orders at exactly that price share the rest in proportion to their size, however many batches the opening takes. If orders are cancelled before the opening batch trades, on chain or because a withdrawal request left an account short of margin, the exchange plans the result again over the orders left (auction.revised_at) and the oracle heads for the new price; if the orders left no longer match, the result is held for review. The market then trades like any other. If nothing crossed, it opens at the listing price without an opening batch. - A held result. A thin or one-sided result, or one pinned at the edge of the collar with more demand beyond it (
auction.outcomeheld), waits for the exchange. It may accept the result, which then opens as above, or extend the auction: the market returns to its pre-open with a later close and often a collar moved toward the demand, and your orders stay.
The place-order request
POST /v0/orders (authenticated). The order must carry an EIP-712 signature — see Authentication & signing. A representative limit order:
POST /v0/orders
{
"market_id": 1,
"side": "LONG",
"order_type": "LIMIT",
"price": 2000,
"quantity": 0.5,
"leverage": 2,
"expiration_seconds": <Unix seconds, at most 14 days ahead>,
"salt": "123456789",
"signature": "0x...",
"signature_type": 1
}| Field | Meaning |
|---|---|
market_id | The market to trade, from GET /v0/markets. |
side | LONG (buy) or SHORT (sell). |
order_type | MARKET, LIMIT, or a conditional type (below). |
price | Limit price in human units. Required, and positive, for LIMIT and the *_LIMIT conditional types; omit it or send 0 for market types. Must be a multiple of the market's tick size (the smallest price increment). |
quantity or quantity_e18_raw | Signed order size in base units, either as a decimal quantity or as an unsigned 1e18-scaled raw integer. Placement is quantity-only: use the estimate endpoints before signing when you want to size from a USD notional, a margin percentage, Max, or a reduce percentage. Quantity must be a multiple of the step size and at least the minimum order size, except for exact reduce-only full closes. An order without reduce_only must also be worth at least $5 (see minimum order value). |
leverage | Position leverage — how many dollars of exposure per dollar of margin (collateral) you put up. A positive whole number, capped by the market's maximum; defaults to 1. It must match the leverage of your existing position and of your other orders in that market, including conditional orders still waiting for their trigger; while a leverage change is pending, it must match the new leverage. |
expiration_seconds | Required. The Unix timestamp (in seconds) at which the order expires, the same value signed into the order: in the future and, by the exchange's clock, at most 30 days (2,592,000 seconds) ahead for a take-profit or stop-loss (the TAKE_PROFIT_* and STOP_LOSS_* types) and at most 14 days (1,209,600 seconds) ahead for every other order. Every order carries one; there is no good-til-canceled order, and 0 is refused. At its expiry the exchange cancels the order, a conditional one too, triggered or not, so sign and place a new order before a resting one expires. The MNX app and the @mnx-markets/mnx-client library pick the expiry when you leave it to them: 60 seconds for a market order and 7 days for any other order, except that the MNX app gives a take-profit or stop-loss 30 days less one hour (the hour keeps a device whose clock runs a little fast inside the limit). |
trigger_price | Required, and positive, for conditional order types. It may not exceed the market's maximum order price, and scaled by 1e18 it must fit a uint128. Omit it for MARKET and LIMIT; a 0 there means no trigger, which is what those orders sign. |
reduce_only | If true, the order may only shrink an existing position, never open or grow one. How much it may close depends on whether it can rest on the book. A MARKET order, such as a position close, may be as large as the position it reduces, whatever other orders you have resting. A LIMIT order may be as large as what your other resting reduce-only LIMIT orders on that side leave of the position, so your resting reduce-only orders never add up to more than it when placed; orders that are not reduce-only, and conditional orders waiting for their trigger, do not count. A conditional order may be as large as the position when placed, and when it triggers it is sized to the position then, whatever rests beside it; a take-profit, stop-loss or reduce-only stop-market order waits while your other triggered ones on that side leave less than one lot or the market's minimum order size of the position, and is otherwise sized as any other; a batch fills them together up to the position at most. Of those, an open one does not count while the oracle price is more than the trigger band short of its trigger, when no batch can fill it, and one that went back to waiting counts only while it rests before it may trigger again and the mark price still meets its trigger. Once the trigger of an entry order's own stop-loss is reached, the unfilled part of that entry is cancelled, whether the stop-loss triggers then or waits; while part of the entry is filled in a batch that has not settled yet, this happens once that batch settles or is aborted, before the next batch, if the stop-loss's trigger is still reached then. If nothing of the entry has filled, its take-profit and stop-loss are cancelled with it. A triggered reduce-only STOP_LIMIT, take-profit or stop-loss limit then rests like any reduce-only LIMIT order and counts against later ones; a MARKET close is still accepted. A batch fills your reduce-only orders together up to the position at most, and once the position is closed, the reduce-only orders left on that market are cancelled, except untriggered take-profit and stop-loss legs of an entry order that is still open. Except for a take-profit or stop-loss sent with protection_scope, one larger than it may close, or with no position to reduce, is refused. |
cancel_on_revert | Optional; best omitted. Accepted and stored but has no effect: an order in a batch that reverts on chain is handled the same way whatever it says. |
builder | Optional. The builder code of the app placing the order: the builder's exchange account address, 0x and 40 hex digits in either case. The exchange stores it lowercase on the order, which returns it as builder (null or absent when the order has none), for builder attribution. It is not part of the signed order and changes nothing about how the order trades. Resending a signed order with a different builder is refused with HTTP 409, except that a top-up request answered from recovery, after the exchange has dropped its first reply, returns the order with the builder it was placed with. Child-vault orders do not take it. |
salt, signature, signature_type | The EIP-712 signature material; the salt and expiration must match the values signed into the order struct. |
The server validates the signature, checks the order against the market's parameters (tick size, step size, minimum and maximum order size, and leverage cap), and reserves margin for it. The price band around the oracle price limits where a batch can clear, not whether an order is accepted. A limit price must lie within the market's minimum and maximum order price (inside 0 to 1 on a binary market); the server refuses one outside them. The part of an order that opens or increases a position reserves full margin up front; the part that closes an existing position reserves no margin, since it reduces exposure. A conditional order that is not marked reduce_only reserves full opening margin even if it would close a position. The exact market parameters are returned by GET /v0/markets.
Minimum order value
An order without reduce_only must be worth at least $5: its whole quantity times its limit price; for a MARKET order, times the price the book clears that whole quantity at when it arrives, or the market's mark price when the book cannot fill it; and for the *_MARKET conditional types, times the mark price. A sell on a binary market is valued at its quantity times one minus that price, the collateral it puts up. This includes an order that would only close part of a position, and conditional orders. A smaller order is refused with HTTP 400 and the message Orders must be worth at least $5 unless they are reduce-only. Reduce-only orders are exempt from this minimum, so close a smaller amount with reduce_only: true. A market order is valued, and its margin reserved, at the book's price for its quantity when it arrives, so size it from a fresh book (GET /v0/markets/:market_id/book), not the mark: a buy clears at or above the best ask, and a sell at or above the bid where the bids, best first, cover its quantity. Leave some room for the book to move. The estimate endpoints, including Max, can return a quantity worth less than $5; check the value before signing an order at that quantity.
Limit orders have no post-only, immediate-or-cancel, or fill-or-kill option; only MARKET orders are immediate-or-cancel. The API rejects post_only: true, a time_in_force of FOK, and IOC on a limit-type order. The deprecated fields still accept post_only: false, time_in_force: "GTT", and IOC on a market-type order, none of which change anything.
Two helper endpoints let you pre-check and size an order before signing: POST /v0/orders/calculate-quantity (legacy helper for converting a USD budget into a quantity, including an estimated clearing price) and POST /v0/orders/estimate (the sizing-intent endpoint for typed notional, typed value, typed units, available margin, Max, and reduce-position percentages). With TARGET_VALUE, a binary future sell without reduce-only counts the contracts that close your long at the price, and the rest, which open or add to a short, at one minus the price per contract, which is what an opening contract puts up at 1x, at any leverage. The long counted is the position after the batch in flight, and the response names it in target_value_long_e18_raw. Flat, $100 sold at 0.125 is 114 contracts; against a long of 100 it is 200, $12.50 to close the long and $87.50 for 100 more. Any other order is sized as with TARGET_NOTIONAL, quantity times price. Every estimate response also includes projected_position_size_e18_raw, your signed position on the market after the batch in flight (negative for a short, "0" when flat), which is the position the estimate's sizing and margin checks read. Placement still sends only the signed quantity plus order fields; do not send usd_amount, sizing_intent, sizing_value, or margin_fraction to POST /v0/orders. Use quantity_e18_raw when the exact signed quantity must avoid decimal-number rounding, such as a 100% reduce-only close of an off-step position. If both quantity forms are supplied, they must match exactly. Send size: 0 to the estimate endpoint when only maximum sizing is needed; limit-order estimates still require price.
Order types
Beyond MARKET and LIMIT, six conditional types wait for the market's mark price (used for unrealized profit and loss and conditional triggers) to cross a trigger before entering the matching pipeline:
| Type | Executes as | Reduce-only |
|---|---|---|
STOP_MARKET / STOP_LIMIT | MARKET / LIMIT | Optional |
TAKE_PROFIT_MARKET / TAKE_PROFIT_LIMIT | MARKET / LIMIT | Always |
STOP_LOSS_MARKET / STOP_LOSS_LIMIT | MARKET / LIMIT | Always |
Conditional orders rest with status PENDING_TRIGGER. A conditional order whose trigger the current mark price has already crossed is rejected at placement. Take-profit and stop-loss orders are reduce-only and reserve no margin; a STOP_MARKET or STOP_LIMIT order that is not reduce-only reserves full opening margin up front. Triggers are evaluated after each mark-price update. Stop and stop-loss orders trigger when the mark price moves against the order's direction (mark ≥ trigger for a buy, mark ≤ trigger for a sell); take-profit orders trigger on the favorable crossing (mark ≤ trigger for a buy, mark ≥ trigger for a sell). On trigger, the order is checked again. If it passes, it becomes a regular market or limit order with status OPEN and enters the next batch, unless the oracle price is still more than the market's trigger band (by default its price band) short of the trigger: the chain refuses that fill, so the order stays OPEN and joins the first batch after the oracle price comes within the band. If it fails its checks, it is rejected and cancelled (see WebSockets). At trigger, a conditional order that is not reduce-only is checked as one that opens its whole quantity, even if it would only close a position: while that position is too close to liquidation, as it can be when a stop meant to close it fires, the order is refused with reason underwater_position. A reduce-only STOP_MARKET or STOP_LIMIT order is checked as a close instead, with no opening check: it can be placed for at most the position on its closing side, and at trigger it is sized to that position as it is then, never more than its signed quantity. So send a stop that closes a position with reduce_only: true. In the MNX app, the Stop Market and Stop Limit forms have a Reduce only box, checked by default when the stop's side and size would close or reduce the open position. If a batch holding a triggered take-profit or stop-loss leg, or a triggered reduce-only STOP_MARKET order, is aborted, it returns to the book; any other triggered market order is handled like a plain market order: one more try in some cases, as described above, and otherwise cancelled with reason batch_aborted. A triggered TAKE_PROFIT_MARKET or STOP_LOSS_MARKET order, or reduce-only STOP_MARKET order, that a batch fills none of is not cancelled: it goes back to PENDING_TRIGGER, keeping the fills it already has, and triggers again once its trigger holds and another account's resting liquidity is there, no sooner than 400 ms after it went back, or twice as long for each further time a batch had no order on the other side or no clearing price for it since it last filled or was left out, up to 10 seconds. A fill starts that count over, and so does a batch that left it out: one that gave the liquidity to other orders, whether or not their fills then went through, or one in which your other orders took the position it would close, whatever was on the other side. It may then trigger again 400 ms later, but triggering again does not mean it sells: a batch can leave it out each time, for as long as other orders win that liquidity, as opening orders can while the market is at its open interest cap even when their fills are then dropped. One that a batch's fill limit cut off stays OPEN for the next batch instead. Each time it triggers it is sized again, to what it may still close: the position then, its signed quantity less what it has filled, and for an entry's take-profit or stop-loss what the entry filled less what its exits sold. One that a batch fills in part stays OPEN with the rest, which a later batch fills or puts back to waiting; while the oracle price is more than the trigger band short of its trigger, no batch includes it. One that would trigger for less than the market's minimum order size is cancelled instead, with reason below_minimum_fill, since no batch could fill it, unless that size is the whole position in whole lots: a fill that closes the whole position is accepted below the minimum. One that your other take-profit, stop-loss and reduce-only stop-market orders leave less than one lot or the minimum waits for them instead. A plain market order and a STOP_MARKET order that is not reduce-only still leave immediate-or-cancel. A market-type conditional order triggers only once another account has a limit order on the opposite side within the price band that has rested (live 400 ms, or longer while the market's previous batch settles) and is not an entry order its own stop-loss is cancelling at the same time, and no conditional order triggers while trading is paused or its market is in cancel-only mode. While trading on the market is halted on chain, no conditional order activates, even when its trigger is reached: it stays PENDING_TRIGGER and is checked again once trading resumes. While the market is resolving a bankruptcy, a *_MARKET conditional whose trigger is reached waits the same way, and a *_LIMIT one becomes a limit order that rests until trading resumes.
An account can rest up to 50 reduce-only orders in one market, conditional ones included whether or not they have triggered; liquidation orders MNX places on the account do not count. Once it rests 50, a new reduce-only order is refused with HTTP 409 and details.reason reduce_only_order_limit_reached, with the limit in details.max_resting_reduce_only_orders (a best_effort bulk item gets only error.code 409), until one of them ends. One plain reduce-only MARKET order that is not conditional, such as a position close, may still go one past it. On POST /v0/orders, an order that is also larger than it may close (the position, or for a LIMIT order what your other resting reduce-only orders leave of it) is refused by that check first, with a plain 400, unless it is a position-scope take-profit or stop-loss, which skips that check.
The response
A successful placement returns the accepted order — including its order_id and order_hash, which you need for cancels and status reads — plus a fills array (empty at placement), an updated balance snapshot when available, and a read_after bookmark you can pass to subsequent reads for read-your-own-writes consistency (see Getting started). Because matching happens at the next batch, expect fills to arrive after placement, not in it.
Every successful POST /v0/orders response also carries is_duplicate. false means this request created the order; true means it placed nothing because the order already existed. Resending the same signed order through POST /v0/orders returns the first response again while the exchange still keeps it, which it does for ten minutes after the order is accepted, even if the order has since been cancelled or filled: its order is the snapshot from the first response, not its current state, so read GET /v0/orders/:order_id or GET /v0/orders/:order_id/events for that. Otherwise, a resend of an order that is still open or waiting for its trigger returns the order's current state; so does a resend of an order placed through bulk placement, whose response is kept under its client_request_id instead. Both say is_duplicate: true. The placement checks still run first, so a resend that now fails one gets that error instead, and the message text is recomputed for each request, so a response returned again can carry a different message from the first one. After ten minutes, resending an unexpired order that has been cancelled or filled is refused with a 409 (details.reason terminal_order_hash_reused). A take-profit or stop-loss that protects the whole position (protection_scope: "position") and names the leg it replaces in replaces_order_hash cancels that leg and places itself in a single step. It answers the same way: false from the request that swaps the legs, true from a resend within ten minutes of the swap or while the new leg is open or waiting for its trigger. After ten minutes, though, a resend whose new leg has been cancelled or filled is refused with a 404 (details.reason position_protection_replace_not_found), not with terminal_order_hash_reused: a replace is checked first against the leg it names, and that leg is gone.
The combined margin top-up and order request (POST /v0/orders/place-with-top-up) returns its first response whole, message included, when an identical request is resent within ten minutes, with its own is_duplicate: true. Otherwise it looks its order up first. An order that was already accepted, whether it is still open, waiting for its trigger, or already cancelled or filled, is not placed again, and the request submits no top-up: the response has order: null and an accepted_order with the order's order_hash, its order_id (null once the order has closed) and its terminal_status (null until then). Its top_up.status is unknown, with a failure_reason saying that this request submitted no new top-up, unless the same signed top-up was submitted before, in which case it reports that top-up's status. The nested order is a placement response only when the request's own order leg ran. Its is_duplicate is true when another request placed the order while the combined request was in flight, and the nested response is then the first response or the order's current state, as above.
Bulk placement reports is_duplicate for the whole request: true when a retried client_request_id returns the first response. Retry a bulk request with its client_request_id: bulk placement does not recognize an order resent under a new one. An order the exchange still holds, open or waiting for its trigger, is refused with a 409, and so are an unexpired order that has been cancelled or filled and a second copy of an order within one request. In best_effort mode the refusal is that item's error, which carries only a code and a message. In atomic mode it refuses the whole request, and the error's details.reason is duplicate_order_hash_reservation or terminal_order_hash_reused.
A market that cannot trade refuses some new orders with HTTP 409. While trading on the market is halted on chain, market and limit orders are refused with details.reason trading_halted_on_chain. While the market is resolving a bankruptcy, market orders are refused with bankruptcy_hold; limit orders are accepted and rest until trading resumes, and the response message says so. Conditional orders are accepted in both states, so a stop or take-profit can still be placed or replaced: it waits as described above, and the response message says when it will not trigger until trading resumes. The exchange re-reads the halt every 15 seconds and stops enforcing a halt it has not confirmed within the last 60 seconds. Cancelling is not affected.
Bulk placement
POST /v0/orders/place/batch accepts up to 50 signed orders in one retry-safe command. Send a stable client_request_id (no control characters), an orders array, and a mode of atomic (the default) or best_effort. Atomic mode means every order is accepted into the batch-auction order book together or none are; it does not guarantee that every order fills. Market orders remain immediate-or-cancel at the next auction tick. Each order must meet the minimum order value: in atomic mode one order under it refuses the whole command, and in best-effort mode only that order is refused.
The response returns one final balance snapshot for the whole command. Accepted items contain their order and read_after bookmark; they do not repeat the same balance or include fills, because matching happens after placement.
Cancelling orders
POST /v0/orders/cancel/by-hash/:order_hash— cancel one order by its hash.POST /v0/orders/cancel/all— cancel all of your open orders, optionally scoped with amarket_id.POST /v0/orders/cancel/batch— cancel a specific list of up to 100orderHashesin one call. Send a stableclient_request_id(no control characters) and reuse it when retrying the same request. Authentication authorizes the cancellation; no separate cancellation signature is required.
Bulk-cancel clients must use the current request shape: client_request_id is required and the former cancelSignature field has been removed. Requests using the old strict schema are rejected.
cancel/by-hash and cancel/all accept an optional cancellation reason of up to 200 characters; longer input is rejected, and control characters in it are stored as spaces. A reason the exchange gives its own cancellations, such as expired or batch_aborted, is rejected with a 400 that lists them. cancel/batch does not accept a reason.
A cancel can arrive after the next batch has already matched the order. It cannot take that match back: the matched quantity settles and fills (unless its batch fails), and the cancel removes only the unmatched rest. cancel/by-hash, each accepted cancel/batch item and each order that cancel/all lists report which happened in status:
cancelled— the cancel took the whole order; nothing more of it fills.cancel_pending_finalize— a batch had already matched part or all of the order.sealed_in_flight_at_cancel_e18_rawis the matched quantity still settling, andcancelled_quantity_e18_rawthe unmatched rest the cancel removed ('0'when the batch had matched everything still open). The order then endsFILLED, orCANCELLEDwith what filled; if the batch fails, nothing more fills and the cancel ends the order.
The by-hash response's cancelled is true only for status: cancelled, and its message says which outcome occurred.
The counts include only orders cancelled outright. In the cancel/all response, cancelled_count counts the cancelled orders and cancel_pending_finalize_count the ones a batch had already matched, and the message says both. In the cancel/batch response, cancelledOrders and cancelPendingFinalizeOrders count its items the same way, and success is true when the request cancelled at least one order outright.
Of the three cancel routes, only cancel/batch recognizes a retry: resending the same client_request_id replays the original result with is_duplicate: true. The other two have no retry key, and their is_duplicate is always false. A by-hash cancel for an order that is no longer open returns 404, for example a retry of one that already succeeded. For your own order, the 404's details.terminal_status says how it ended when the exchange can tell: FILLED, CANCELLED or EXPIRED. Repeating cancel/all cancels nothing. After a lost response, confirm the order's state with GET /v0/orders/:order_id.
Reading order status and fills
Order status is one of OPEN, FILLED, CANCELLED, EXPIRED, PENDING_TRIGGER, or CANCEL_PENDING_FINALIZE (a cancel accepted while a batch containing the order is settling).
GET /v0/orders/by-user/:user_id— your orders, filterable by market, status, and side.GET /v0/orders/:order_id— one order's current state.GET /v0/orders/:order_id/fills— the fills (individual matched executions) of one order.GET /v0/orders/:order_id/eventsandGET /v0/users/:user_id/order-events— the lifecycle event log (CREATED,PARTIALLY_FILLED,FILLED,CANCELLED,EXPIRED).GET /v0/orders/:order_id/fill-processing-status— whether fills are still being settled on-chain.
For push-based updates instead of polling, subscribe to your user topic on the WebSocket feed — see WebSockets.