Surface Map
Spot
Spot trading uses balances deposited into Monaco’s vault and settles swaps on Monaco’s orderbook.
Perps
Perps trading uses collateral, margin, leverage, funding, positions, and liquidation controls.
Order Entry
Place, cancel, replace, batch, and monitor spot and perps orders.
Account Balances
Read available, locked, and total balances before enabling order entry.
Before You Build
- Use Wallets & Auth, Account Balances, Vault Deposits & Withdrawals, and Perps Collateral to understand wallet funds, Monaco spot balances, and perps collateral.
- Use Markets and Orderbooks & Trades to resolve pair IDs, product support, order constraints, and market data.
- Use REST API, WebSocket API, gRPC API, or an SDK for markets, orderbooks, trades, candles, balances, orders, and live account events.
Shared Trading Flow
Most spot and perps trading interfaces follow the same high-level loop:1
Load markets and account state
Query supported trading pairs, product support, market metadata, and the user’s balances or collateral before enabling order entry.
2
Render order controls
Use tick size, order-size limits, available balance, leverage constraints, and product support to validate inputs before submission.
3
Submit, replace, or cancel orders
Place market or limit orders, cancel stale orders before replacing intent, and use batch endpoints for active interfaces and agents.
4
Stream updates
Subscribe to order, balance, orderbook, trade, OHLCV, position, and movement streams so UI state tracks Monaco state.
Self-Trade Prevention
What happens when your incoming order would match one of your own resting orders is selectable:CANCEL_MAKER (cancel your resting order and keep matching — the platform default), CANCEL_TAKER, CANCEL_BOTH, or SKIP (trade around your own orders). Pass selfTradePreventionMode on any create, replace, or batch item, or set a wallet-wide default with PUT /api/v1/accounts/self-trade-prevention (sdk.profile.setSelfTradePreventionDefault). “Your own” spans the whole wallet family — every subaccount and application sharing the wallet address. STP outcomes are cancellations, not errors — the affected orders terminate as CANCELLED with terminalReason: "SELF_TRADE_PREVENTION" — with one exception: a FOK order under CANCEL_BOTH whose fill path contains your own order is rejected with SELF_TRADE_NOT_ALLOWED. Full semantics, mode by mode: Self-Trade Prevention.
Spot Trading
Spot trading uses balances deposited into Monaco’s vault. A spot integration usually needs account balances, supported markets, order entry, order management, and live order or balance updates.Spot Flow
1
Load markets and balances
Query supported markets, then load authenticated user balances so your UI can enable only markets and assets the user can trade.
2
Place orders
Submit market or limit orders against a spot trading pair. Use time-in-force and slippage controls based on the UX you expose.
3
Manage open intent
Cancel stale orders before submitting replacement intent. Use batch endpoints for active interfaces and agents.
4
Stream updates
Subscribe to order, balance, orderbook, trade, or OHLCV channels so the UI updates as Monaco state changes.
Spot SDK Entry Points
Perps Trading
Perps use a separate perps account for collateral and risk. A perps integration should make collateral, leverage, liquidation risk, funding, and reduce-only behavior explicit before users place orders.Perps Flow
1
Load collateral and positions
Query perps collateral, positions, funding, and risk state before enabling leveraged orders.
2
Preview risk
Simulate meaningful orders before submission so traders can see projected margin, free collateral, and liquidation impact.
3
Place or reduce exposure
Submit market or limit orders with the correct leverage and reduce-only settings for the user’s intent.
4
Monitor continuously
Stream order, position, balance, and market updates. Perps UIs should keep mark price, margin ratio, funding, and liquidation distance current.
Perps SDK Entry Points
Order Entry and Management
Orders are the shared execution primitive for spot and perps. The same SDK methods submit market and limit orders; spot orders use Monaco balances, while perps orders add leverage and reduce-only controls (side sets the one-way direction).
Use this section for order tickets, quoting interfaces, market-maker systems, and agent workflows.
Delegated-agent sessions can place, cancel, and replace orders only when the active policy allows the requested action and market scope. Monaco re-checks the delegation on every order mutation, so revocation or expiry blocks new delegated trading even if the delegated auth session itself has not expired.
Order Concepts
Place Orders
Trading operations require an authenticated Monaco session. Resolve a trading pair UUID first, then pass that UUID into order methods.Limit Orders
Limit orders execute at the specified price or better. They provide price protection, can rest on the book, and default toGTC.
Market Orders
Market orders execute immediately against available liquidity. Use slippage protection when the user cares about maximum price movement.Time in Force
Time-in-force controls how a limit order behaves when it cannot fully execute immediately.GTC when you want price protection and are willing to wait. Use IOC when you want immediate execution but can accept a partial fill. Use FOK when partial execution is worse than no execution.
Time-in-force options apply to limit orders. Market orders execute immediately and use slippage protection instead.
Post-Only Orders
postOnly: true guarantees an order can only add liquidity — it is a maker-only flag, not a new order type. Pass it alongside a normal limit order (spot or perps) to reject the order instead of letting it take: if the price would cross the book at admission time (BUY at or above the best ask, or SELL at or below the best bid), Monaco rejects it — REST 400, gRPC InvalidArgument — with the structured engine code POST_ONLY_WOULD_CROSS instead of matching. For a single placeLimitOrder or replaceOrder call, that code rides on the response: REST puts it in the error envelope’s optional code field, and gRPC attaches a google.rpc.ErrorInfo status detail with reason set to the code and domain set to matching-engine.0xmonaco.com. Match on the structured code; the rejection message still contains “post-only order would cross”, but substring-matching it is a legacy fallback, not the contract. Batch Create and Batch Replace carry the same code per item: each per-item result’s structured error.code is set to POST_ONLY_WOULD_CROSS for a post-only crossing failure, as before. A non-crossing postOnly order is admitted and rests exactly like an ordinary limit order of the same time-in-force. The flag is persisted with the order and echoed back on order detail and order list responses as postOnly: true — like reduceOnly, it is absent when the order was not placed post-only (including orders placed before the flag was persisted).
postOnly only composes with LIMIT orders using GTC time in force. It is rejected on MARKET orders and rejected when combined with IOC or FOK (both are immediate-or-cancel semantics that contradict a maker-only guarantee).
postOnly for market-maker quoting and for rebuilding quotes around a fresh price (for example, right after a market reopens) where an accidental take is worse than a rejected order you can immediately re-price.
replaceOrder and batchReplace each accept their own postOnly — a replacement re-prices the order, so the caller re-states maker-only intent explicitly; it is never inherited from the original order. See Replace Orders and Batch Replace for how a rejected post-only replacement affects the original order.Safe Retries with Idempotency Keys
The TypeScript SDK always sends anidempotencyKey on placeLimitOrder, placeMarketOrder, and every batchCreate item. It generates a UUID when you omit the key; React hooks and MCP order tools inherit this behavior. This is separate from clientOrderId, which remains a reusable order label.
Each SDK call without an explicit key starts a new submission. For recovery across calls or process restarts, generate and persist your own key before the first call, then reuse it with the same order details. The SDK currently sends each request once; it does not automatically retry order placement. An APIError retains the submitted body, including its key, in requestBody.
A-Za-z0-9._:-, with no surrounding whitespace. They are scoped to your authenticated user/subaccount and application. For 24 hours after acceptance, the same key and normalized order payload return the original committed submission result, even if the order has since filled, been cancelled, or the engine has restarted. This result describes the original submission; use order reads for current lifecycle state. Decimal spellings such as 1.0 and 1.00 are equivalent.
Reusing a retained key with different order details returns IDEMPOTENCY_KEY_CONFLICT (HTTP 409 / gRPC ALREADY_EXISTS). Requests rejected before acceptance do not reserve the key. An accepted order whose match result is REJECTED does reserve it: a retry returns that same result instead of trying to trade again.
Retries do not extend the window. After 24 hours, the same key may place a new order. Do not retry an unresolved submission beyond that window. Without an idempotency key, continue to follow the lost-response guidance below; a clientOrderId alone cannot prevent duplicate execution.
Two further codes describe the storage behind these keys, and both are retryable rather than terminal. ORDER_SUBMISSION_CAPACITY_EXCEEDED refuses a new key when retained submission receipts are at capacity because responses are protected by pending durability or persistence; existing retained receipts still replay while storage is full, and the refusal does not accept or reserve the key, so nothing was applied. Capacity frees as protected receipts finish acknowledgement and persistence, usually within seconds, so retry the same key with bounded backoff rather than waiting for anything to expire. It also refuses a new key when your own share is full: each user and application can hold at most 10,000 receipts (32 MiB of receipt payload) that are not yet evictable, and while one owner is at its share every other owner keeps placing orders; retries of an existing key and orders without a key are unaffected. ORDER_SUBMISSION_UNAVAILABLE means the original response for a retained key cannot currently be retrieved, or keyed admission is busy — read it as unknown, never as a rejection, because a submission under that key may already have executed. Both return HTTP 503 / gRPC UNAVAILABLE on a single create and arrive as a per-item error in a batch, and neither ever authorizes duplicate execution — retry the same key with the same normalized payload. Do not mint a fresh key for an order you already submitted under one: the original submission may still commit, and a new key is a second independently admissible order.
For batchCreate, the TypeScript SDK generates a separate key for each item that omits one. Supply and retain your own per-item keys when you need to retry across calls. Results remain in input order, failures stay per item, and an item’s key also works when retrying that order through single create. A key protects its own item; it does not make the batch all-or-nothing. Only items carrying keys are safe to retry after an ambiguous batch outcome.
A create batch containing any idempotencyKey processes every item in input order through the single-create path. Other submissions can execute between items; the batch has no shared sequence or atomicity guarantee. Results retain input positions, and a failure does not roll back successful siblings. Unkeyed items sent by legacy or raw API clients remain unsafe to retry blindly. Updated TypeScript SDK batches always use the keyed path.
Two-phase rollout
Phase one: REST and gRPC keep the field optional for existing clients. Updated TypeScript SDK calls always send keys; callers using raw REST or generated Rust/Python bindings must generate and retain keys before serialization and signing. Upgrade the migration, persistor/verifiers, matching engine, and all public gateway replicas before releasing clients that rely on this protection. Old gRPC replicas silently ignore the field. Phase two: after client migration, require a key on every single create and every batch-create item at the API boundary. Reject missing keys before order admission; never generate a replacement key on the server. This enforcement is planned, not enabled in phase one. Confirm supported clients and direct integrations send keys before enabling it.Client Order IDs
clientOrderId is a label you choose and Monaco echoes back. Pass it on placeLimitOrder, placeMarketOrder, replaceOrder, batchCreate or batchReplace; read it back on order detail and order list responses, and on the order WebSocket events — the OrderPlaced acknowledgement, taker and maker fills, cancellation, expiry, and the OrderRejected raised after acceptance — an IOC/FOK that cannot fill, or a MARKET order whose apparent liquidity is invalidated by maker-risk revalidation. Because the acknowledgement and the fill both carry it, a matching event on the stream is positive proof that a submit whose HTTP response was lost did land — though only in that direction: silence never resolves the negative case. Maker fills carry the resting order’s own handle, so a quote’s fill identifies itself without a lookup.
It accepts up to 64 characters from A-Za-z0-9._:-. Surrounding whitespace is trimmed, and a blank string is treated as absent. Anything longer, or containing another character, is rejected with a 400 (gRPC INVALID_ARGUMENT). In a batch, both endpoints scope the rejection to the offending item with the same error.code INVALID_CLIENT_ORDER_ID, letting the rest proceed — and a failed batchReplace item never touches its original. (Earlier releases rejected the whole batchReplace request on one malformed handle.)
Uniqueness among your resting orders
AclientOrderId may be held by only one of your resting orders at a time. On a single create or replace, reusing a value another of your resting orders is still carrying is rejected with 409 and the code CLIENT_ORDER_ID_CONFLICT; the message names the order already holding it.
In a batch (batchCreate / batchReplace) the same conflict is reported differently: the request itself succeeds with 2xx/OK and only the offending item carries error.code CLIENT_ORDER_ID_CONFLICT in its results[] slot. There is no transport-level 409 to wait for — inspect results[].
Read “resting” strictly — it means sitting on the book, not merely “recently sent”. The scope is per user, and only among orders that are resting:
- Only a resting order holds a
clientOrderId. An order that never rests — aMARKETorder, anIOCorFOK, or aLIMITthat fills completely on arrival — does not take the handle, so the value stays free and a later create reusing it is accepted. - Two different users may use the same value simultaneously. Uniqueness is never global — your labels are yours.
- The value frees the moment its order reaches a terminal state (filled, cancelled, expired, rejected). A stable per-quote label can be reused indefinitely as you re-quote — but not if you intend to reconcile by it. Reuse makes a matching event ambiguous: the previous holder’s terminal event can still be in flight when the reused value is submitted again, so an event carrying that handle may belong to the earlier attempt. Use a fresh value per create intent when the handle is your reconciliation key. A replace is the deliberate exception — it restates the original’s handle, so distinguish the two by
orderId: the original’s id you already hold, versus the replacement’s new id. - A replacement may restate the original’s value. The original releases the handle as the replacement claims it, so
quote-btc-1130-acan follow your quote across every replace.
Recovering from a lost response
Subscribe before you place. The stream is not replayable: a slow-client disconnect (close code1013) signals a gap on the whole connection, including state streams, and there is no wire sequence number to
backfill from — see WebSockets.
A subscription opened after your request timed out cannot recover the acknowledgement you
missed. Use sdk.ws.userOrders(handler) for every pair at once, or
sdk.ws.orders(pairId, mode, handler) for a single market.
The stream is pushed from the matching engine as each event occurs, and carries your
clientOrderId on the OrderPlaced acknowledgement, on taker and maker fills, on
cancellation and expiry, and on the OrderRejected raised after acceptance — an
IOC/FOK that cannot fill, or a MARKET order whose apparent liquidity is invalidated
by maker-risk revalidation — including for orders that never rested. A MARKET order that fills on
arrival emits two labelled events, OrderPlaced then OrderFilled, so the stream
acknowledges even a shape that never takes the handle on the book.
Rejections split in two, and the difference decides whether the stream can answer for you:
- Rejected before acceptance — a malformed or oversize order, a post-only order that
would cross, a
clientOrderIdconflict, a plainMARKETsubmit with no liquidity inside the protective band, which is rejected during pricing before a sequence number is allocated, or a perp order the perp price band keeps from filling anything. The request fails with an error status, the order never enters the book, and no event is emitted. It is never written to order history either, so it leaves no trace afterwards. That is safe rather than dangerous — nothing executed. - Rejected after acceptance — a
LIMITwithIOCorFOKthat cannot fill on arrival, or aMARKETorder whose apparent liquidity is invalidated by maker-risk revalidation after it was accepted. The request succeeds with a2xx,matchResult.statusisREJECTED, and the stream emitsOrderPlacedthenOrderRejected, both carrying yourclientOrderId.
REJECTED — so a REJECTED row is real information, but its absence
still is not.
If no surface is conclusive, hold rather than resubmit. The two mistakes are not
symmetric: a wrongful resubmit doubles live exposure, while a missed quote can simply be
quoted again.
Where the conflict does fire — a resting order you are re-quoting — it is a reliable signal, because the check runs in the matching engine against live state rather than against replicated history. The limitation is its scope, not its freshness.
Orders placed before the field existed return no clientOrderId at all.
Perps Order Options
Perps use the same order methods with margin-specific options. New integrations can usually let Monaco resolve the parent margin account and risk bucket; explicitmarginAccountId remains available for legacy or fixed-account workflows.
Use
riskBucketId to route an order into an existing isolated risk bucket. Use riskBucketCollateral only on the first order for a new trading pair — when no isolated bucket exists yet; once the bucket is created, omit this field and the backend auto-resolves it. Passing riskBucketCollateral when a bucket already exists returns 400. riskBucketId and riskBucketCollateral are mutually exclusive. Use strategyKey when a bot, strategy, or UI workspace should keep risk separated for the same market. See Margin Accounts for the risk-bucket model.
A riskBucketId that does not exist or is not yours returns 404 Risk bucket not found, checked before anything else about the bucket. For one of yours, a bucket scoped to a different trading pair or margin account returns 400 riskBucketId does not belong to tradingPairId / riskBucketId does not belong to marginAccountId, and a bucket under a non-default strategyKey — a copy trading risk bucket included — returns 400 riskBucketId names a copy trading risk bucket; copy risk buckets are managed by the engine. Route strategy-keyed orders by strategyKey rather than by riskBucketId.
Admission is the one collateral check for a margin order. The risk bucket’s own collateral covers the order first; if it falls short, the engine draws exactly the order’s additional requirement from the parent margin account’s unallocated capacity, atomically with the order. What an order is charged depends on how it trades. A crossing order is charged its initial margin at the mark, the loss each fill takes against the mark (a sell into a bid below the mark, a buy into an ask above it), and its taker fee; a resting order is charged its initial margin at its limit, its maker fee, and the loss it would take against the live mark if it rests past it. Only the part that opens or flips a position is charged: a close is never refused for its loss. The draw covers only what the orders cost, so a position’s loss never stops a new order the parent can fund and is never drawn for; a risk bucket below its initial margin stays below it until you add collateral or reduce. Once admitted, an order is never rechecked or topped up when it fills, whether it fills as a taker, as a resting maker, or rests after partially crossing. A fill keeps in the risk bucket only what its new position needs and returns the rest of the draw; a cancel, smaller replace or IOC remainder returns everything it releases. Liquidation freezes still reject new orders. No client flag is required; use risk simulation and availableOrderCollateral for pre-trade sizing rather than assuming parent headroom guarantees admission.
Risk Buckets and Pre-Trade Simulation
A parent margin account holds reusable collateral. Orders create or reuse pair-scoped risk buckets under that parent. New integrations can preflight orders without storing amarginAccountId.
Use parent simulation when the order should draw from the user’s general parent margin collateral:
POST /api/v1/margin/risk-buckets/simulate-order-risk) accepts marginMode: "ISOLATED" (default) or marginMode: "CROSS". A cross preview names no pairs: cross scope is derived from trading, so the previewed pair is in scope by construction and the response’s selectedTradingPairIds reports the derived scope. Isolated simulation models the risk bucket a first order would create when none exists yet; cross simulation works before the user has placed their first cross order. For parent-margin simulation without a specific bucket, use simulateParentMarginOrderRisk above.
For isolated simulation, estimatedLiquidationPrice is the simulated position/risk-bucket threshold. For marginMode: "CROSS", it is conditional per target position, not a whole-account scalar: it varies only the target position’s mark while all other marks in the cross risk bucket remain unchanged. Other position marks, funding, realized PnL, fees/reserves, and collateral can change it. Render an absent or blank estimate as unavailable, never 0.
rejectReason directly, and block submission when the simulated order is not accepted.
Reduce-Only
reduceOnly: true guarantees the order can only shrink an existing position. It cannot flip a position or grow exposure. If the reducing portion is no longer available by the time the order matches, the remainder is cancelled instead of extending risk.
Common uses:
- Closing a position with a market order
- Placing a limit close at a target price
- Implementing manual stop logic that fires a reduce-only market order
Conditional Perps Orders
Take-profit and stop-loss orders are conditional perps orders. They sit dormant until the mark price crosses a trigger, then submit the underlying market or limit order. Conditional orders trigger on mark price:- TP on a long triggers when mark rises to or above
triggerPrice. - TP on a short triggers when mark falls to or below
triggerPrice. - SL on a long triggers when mark falls to or below
triggerPrice. - SL on a short triggers when mark rises to or above
triggerPrice. - A trailing stop on a long triggers when mark falls to or below
watermark × (1 − trailBps/10000). - A trailing stop on a short triggers when mark rises to or above
watermark × (1 + trailBps/10000).
simulateParentMarginOrderRisk or simulateRiskBucketOrderRisk: expectedMatchResult.actualSlippageBps is the impact the current book would charge that size (apply it to your trigger price, not the current mark) and estimatedFee is the close leg’s fee at your tier — see Pre-trade simulation.
Attach TP/SL to an open position with attachPositionTpSl:
oco: true to make the two legs one-cancels-other: when one triggers, the sibling is cancelled (requires both legs). Set closePosition: true on a leg to resolve the full live position size on trigger. Subscribe to conditional-order events and regular order events to confirm both the trigger and the resulting fill.
A full-close (closePosition) leg attached this way supersedes the position’s previously active full-close legs of the same condition type: attaching a new closePosition take-profit cancels the old closePosition take-profits atomically in the same engine step, and likewise for stop-losses — so moving a TP is one call, with no window where the position holds both (or neither) of the two levels. Each superseded leg announces itself with a conditional_order_update event carrying reason: "cancelled" before the replacement’s "created".
Conditional creation is also subject to resident cardinality limits: by default 10 active legs per position, 100 resident legs per user on an engine shard (including pending entry-order legs), and 100,000 resident legs on that shard. Deployments can override the defaults. Position attaches check all three; entry-order TP/SL placement checks the user and shard limits before admission. A create that would exceed a limit creates none of its new legs. Entry-order placement reports REST 400 / gRPC INVALID_ARGUMENT (or a per-item batch rejection); the position-attach adapter instead reports an engine cap refusal as generic REST 500 MATCHING_ENGINE_ERROR / gRPC INTERNAL. That status alone does not identify a cap refusal. Full-close legs superseded by the attach are subtracted before the cap check, so moving an existing level at the cap does not require an extra slot. Cancel unused fixed-quantity legs rather than retrying a known over-cap create unchanged.
Three things deliberately survive a supersede:
- the superseded leg’s OCO sibling — a moved TP does not silently drop your stop;
- every fixed-
quantityleg — laddered partial take-profits stack; only full-close legs replace each other; - legs attached to an entry order (below) rather than to the position. These are excluded in both directions: a position-attached leg never cancels them, and they never cancel a position-attached leg. A leg on an unfilled entry order is not armed yet, so having it retire live protection that its own order might never replace would be the more dangerous default. If you keep full-close TP/SL on both an entry order and the position, expect both to be armed once the entry fills.
takeProfit/stopLoss on placeLimitOrder / placeMarketOrder (margin entry orders only — not reduce-only). The close direction follows the order’s side. If the entry order rests, the legs are created pending and activate when the order fills; they are cancelled automatically if the order is cancelled or expires unfilled. When both legs are supplied they are OCO-linked. The response returns takeProfitOrderId / stopLossOrderId.
Each entry-attached leg sizes itself with the same contract as a position-attached leg: omit both quantity and closePosition (or set closePosition: true) to resolve the full live position size when the leg triggers, so it keeps covering the position as it grows; or set a quantity for a deliberate partial that never resizes. The two are mutually exclusive, and quantity is required when closePosition is false. (Before this contract existed, an entry-attached leg was frozen at the entry order’s quantity, so a position that later grew past it kept a stop covering only part of what was held.)
Trailing Stop
A trailing stop is a reduce-only market close that fires when the mark price retraces from its best level since arming bytrailBps.
- Long: trigger = watermark × (1 − trailBps/10000), fires when mark falls to or below it.
- Short: trigger = watermark × (1 + trailBps/10000), fires when mark rises to or above it.
- The watermark is the best mark seen since arming, so the trigger only ever moves in the position’s favour.
activationPrice(optional) starts tracking when the mark reaches that level and seeds the watermark there; it is rejected if the mark has already reached it. Omitted, tracking starts at the current mark.- One trailing stop per position — a second attach returns
409— and there is no modify: cancel it and attach a new one. It coexists with fixed TP/SL and never joins OCO. - Attached to an entry order, it arms only when the order fully fills and is cancelled with the order otherwise.
trailingStopOrderId, though an entry order returns it only when the trailing stop materializes: the order fully fills on arrival or leaves a resting remainder; a partial fill whose remainder is cancelled (an IOC or MARKET order) creates no trailing stop and omits the id. The row reads conditionType: "TRAILING_STOP" and, once armed by its position (or by its entry order’s full fill — until then it waits as PENDING_PARENT), stays state: "ACTIVE" while waiting for activation, with trailArmedAt set once tracking begins and watermarkPrice next to the live triggerPrice. On the conditional_orders channel it emits reason: "armed" when tracking begins and reason: "ratcheted" on every trigger move, each frame carrying the new triggerPrice and watermarkPrice.
Cancel and List Conditional Orders
Conditional Order Lifecycle
Trigger Price Is Not Fill Price
A conditional order fires on the mark price and then trades against the book, so the price it triggered at and the price its close filled at are two independent numbers. The mark is a reference price, not the book’s midpoint, and the trigger worker samples it on a tick, so by the time the close is placed the mark may already be well past the level. AMARKET leg then pays the spread it crosses plus whatever depth the size consumed — a large close walks several levels — which usually leaves it worse than the trigger price, but there is no guaranteed minimum gap and a fast move can leave it better. A LIMIT leg may rest (SUBMITTED / PARTIALLY_FILLED) or fill only partly, and a MARKET leg that finds nothing fillable inside the 1,200 bps protective band is cancelled with a terminalReason. Round-trip taker fees apply on top, so a take-profit set within a tick or two of entry can trigger exactly as set and still close at a loss.
The read surfaces carry both sides so the two never have to be reconciled by hand:
- A
TRIGGEREDconditional order (getConditionalOrderandlistConditionalOrders) carriestriggeredOrder— the close’s currentstatus,filledQuantity,averageFillPrice,totalTakerFees,filledAtandterminalReason, read from that order at request time. ComparetriggeredOrder.averageFillPricewithtriggerPriceto see the spread and impact the close actually paid. Compare them as exact decimals rather than with===on the raw strings: the two reads can print different scales, and because this summary is a repository read whilegetOrderis cache-first, persistor lag can also leave them showing different states — readgetOrderfor the close’s very latest. The field is absent until the trigger fires, and is not carried on theconditional_ordersWebSocket frames. - The close itself carries
conditionalOrderIdin order history (getOrder,getPaginatedOrders,orderType: "MARKET"or"LIMIT"), pointing back at the take-profit or stop-loss that placed it. It is absent on orders you placed yourself and on triggered closes written before the field existed — there is no backfill, so a missing value on an older close means unknown, not placed by hand.
Manage Orders
Order management is part of the active trading loop. Monaco supports single-order cancellation, batch cancellation, cancel-all, replacement, batch replacement, and batch creation.List Orders
GET /api/v1/orders (sdk.trading.getPaginatedOrders) lists the caller’s book orders — LIMIT and MARKET orders, resting and historical — as one stream sorted by timestamp across the hot and archived stores. Filter status=SUBMITTED,PARTIALLY_FILLED for the resting working set.
TWAP parents and conditional (take-profit / stop-loss) orders are not book orders and never appear in this listing. Read parents from GET /api/v1/orders/twap (sdk.trading.listTwapOrders) and conditional orders from GET /api/v1/orders/conditional (sdk.trading.listConditionalOrders); both filter by state and market. The order a take-profit or stop-loss fires into is a book order: it is listed here as its own MARKET or LIMIT row carrying conditionalOrderId back to the trigger, and the conditional order’s triggeredOrder summarizes what that close did — see Trigger Price Is Not Fill Price.
A TWAP parent’s resting child orders are not listed as separate rows you can act on — the parent is the only control surface, so cancel the parent rather than its children (see TWAP Orders). Likewise, the LIMIT leg a take-profit or stop-loss fired into (a row carrying conditionalOrderId) can be cancelled but not replaced: a replacement would rest under a new id the conditional order does not point at, so PUT /api/v1/orders/{orderId} and batch replace refuse it with INVALID_ORDER. Cancel it and place a new order instead.
Cancel Orders
remainingQuantityTarget for a partial cancel — the resting order is reduced in place to that remaining quantity and keeps its queue position, unlike replaceOrder, which retires the order and rests a new one at the back of the book even for a pure size-down. Only the released delta unlocks.
OrderPartiallyCancelled event on the orders WebSocket channel; the order is still resting.
Monaco prioritizes cancels ahead of new order intent. A cancel only protects quantity that has not already matched; fills that happen before the cancel is processed remain valid.
Batch Cancel
Cancel All
400 / gRPC InvalidArgument. Narrow the request by trading pair or cancel explicit batches of at most 100 IDs. The cap is checked before any cancellation.
Handle these request-level failures before treating the response as a per-order result: REST 400 / gRPC InvalidArgument for an invalid pair UUID or the cap; REST 401 / gRPC Unauthenticated for invalid authentication; REST 403 / gRPC PermissionDenied when a delegated session omits its required pair scope or lacks permission; REST 429 / gRPC ResourceExhausted when the call exceeds your risk-reduction rate budget — cancel-all costs one item per call however many orders it cancels, so this is about call rate, not order count (see Rate Limits); REST 500 / gRPC Internal for an internal matching-engine failure; and REST 503 / gRPC Unavailable for a transient matching-engine or transport failure.
Replace Orders
Replacing an order atomically cancels the original order and creates a new one. The replacement receives a new order ID.quantity is the order’s new total, not a fresh size: on a partially filled order the replacement rests total - filled and locks collateral for that remainder only (FIX cancel/replace convention, LeavesQty = OrderQty - CumQty), so it must exceed the amount already filled — replacing a 96/100-filled order with quantity: "200" rests 104 and reads as 96/200 filled. Omit quantity to keep the order’s total; the resting remainder is then derived against the live fill state, so a fill landing while the request is in flight cannot make the replacement rest more than the true remainder. The replacement also inherits the original’s fill history (filled quantity, VWAP, realized fee/payment aggregates), so summing those aggregates across a replacement chain double-counts — per-order and trade-level reads are unaffected. Margin reduce-only orders follow the same rule: on a reduce-only close of 0.8 with 0.3 filled, quantity: "0.5" rests 0.2 and reads as 0.3/0.5 filled. What a reduce-only replacement rests (total - filled) is still bounded by the live position, so a total that would rest more than the position is refused. batchReplace items carry the same total semantics. Pass postOnly to re-state maker-only intent for the replacement — see Post-Only Orders; it is never inherited from the original order. selfTradePreventionMode follows the same rule: the replacement is a new order and carries its own value, falling back to your wallet default when omitted — see Self-Trade Prevention.
The response’s updatedFields.quantity reports the replacement’s total, not its resting size, on replaceOrder and on each batchReplace result alike. With quantity omitted, that is the original order’s total: repricing a 60/100-filled order reports "100" and rests 40. Releases before v1.0.44 reported the unfilled remainder ("40") in this case; an explicit quantity has always been echoed as given. Read the order’s remainingQuantity when you need the resting size. Margin reduce-only replacements report their total the same way. updatedFields.price is the replacement’s price: the requested price, or the original’s when omitted.
For delegated agents, replacement is checked against both REPLACE_ORDER permission and the resulting order limits, including trading pair or margin account scope, order type, time in force, leverage, and notional. maxOpenOrders is unsupported and is rejected when configuring a delegation.
A single
replaceOrder validates the replacement (including a postOnly crossing check) before cancelling the original. A rejected replacement — including a post-only crossing rejection — leaves the original order resting, untouched. batchReplace is cancel-first and so differs for most failures, but it matches this behaviour for postOnly crossing specifically; see Batch Replace.Batch Create
postOnly order that would cross the book — is reported as a structured { code, message } on that item’s result (error.code === "POST_ONLY_WOULD_CROSS" for a post-only crossing rejection); match on code, the same structured code the single-order endpoints now carry on their error envelope. Validation failures (a malformed trading pair, an unparseable price, and so on) are scoped to their own item too — the same per-item contract as Batch Replace, where a failed item’s original order is left untouched.
A MARKET item must omit timeInForce — it is rejected on market orders, which take no time-in-force and always execute IOC-style inside the protective price band (matching the single-order endpoint).
A batch is capped at 100 items. The SDK rejects a longer array client-side (At most 100 orders per batch request, the exported constant MAX_BATCH_ORDER_ITEMS) before sending, and the REST/gRPC APIs reject an oversized batch with HTTP 400 carrying the same message. Split larger sets across requests. The server-enumerated Cancel All has a separate cap of 20,000 matching active orders.
Each item also costs one order-creation item against your per-account rate budget — reduceOnly items included — so batching saves round trips, not budget. An over-budget batch is rejected as a whole with 429 before any item is processed, never item by item; a raw HTTP or gRPC client can read the x-ratelimit-remaining-* headers on each response to size the next batch, while the TypeScript SDK surfaces only retryAfter on the 429.
Batch Replace
CANCEL_MAKER.
A replacement that succeeds is not necessarily resting: it can be placed and then immediately cancelled by self-trade prevention — under CANCEL_TAKER or CANCEL_BOTH, or as a SKIP remainder that would have rested crossing one of your surviving orders. The item reports success with its new order ID either way, so read the order’s status rather than inferring it from the batch result. A replacement also inherits the original’s time in force rather than taking one of its own; since only a resting order is eligible, that is always GTC/GTD, never IOC/FOK.
Per-order failure reporting applies to placement-time failures — the ones that reach the matching engine. An item whose replacement fails to place (insufficient balance or a risk rejection) reports the error as a structured { code, message }, with its original already cancelled, so such a failed item leaves no resting order behind. Meeting one of your own orders outside the batch is no longer a placement failure: the item’s replacement resolves it through self-trade prevention and places. The request carries no timeInForce input — the replacement inherits the original’s — and only an already-resting original is eligible, so that value is always GTC/GTD and the FOK + CANCEL_BOTH rejection cannot arise here; it belongs to Batch Create. An item whose original order cannot be found is reported the same per-item way (error.code === "ORDER_NOT_FOUND"), and does not roll back other replacements in the batch.
postOnly crossing rejections are the exception, and do not cost you the original. A postOnly replacement that would cross is refused before the cancel pass runs, so its original stays resting exactly as it was — same order ID, same queue position. The item still reports error.code === "POST_ONLY_WOULD_CROSS", same shape as batch create; the difference is only what happens to the original. The check is evaluated against the book as it will stand once the batch’s own cancels are applied, so a two-sided ladder shifting across its own resting prices is unaffected.
Four narrow cases remain, because none of them can be seen before the cancel pass runs. In the first three the item reports POST_ONLY_WOULD_CROSS with its original cancelled, exactly as before:
- The item would cross a replacement that an earlier item in the same batch just rested. This only arises for an internally crossed target ladder — one whose own new bids sit at or above its own new asks — which is a malformed request rather than the “market moved into my quote” case.
- Another item’s original failed to cancel and so stayed on the book, and your replacement crosses that order. The pre-cancel check assumed it would be gone.
- You listed the same original twice with different replacements. Each occurrence is judged on its own price, so a refused occurrence can report
POST_ONLY_WOULD_CROSSwhile a surviving sibling occurrence still cancels the shared original (first occurrence wins, as always for duplicates).
- An earlier item in the same batch would have taken the liquidity your post-only replacement is judged against (a non-post-only replacement that crosses). The check reads the touch as it stands before any placement, so it refuses a quote that would in fact have rested. Your original stays where it is; the requote simply does not happen, and retrying after the batch succeeds.
orderId or numeric field, an item’s postOnly shape violation (a MARKET order, or postOnly combined with IOC/FOK), a malformed clientOrderId, or a delegated-session item without a persisted original each fail alone — that item’s result carries a structured { code, message }, using the same code vocabulary as batch create (INVALID_ORDER_ID, INVALID_PRICE, INVALID_QUANTITY, INVALID_CLIENT_ORDER_ID, INVALID_POST_ONLY, INVALID_SELF_TRADE_PREVENTION_MODE; FORBIDDEN for a delegated item without a persisted original) — and its original order is never touched, while the batch’s valid items proceed. This per-item contract is the raw REST/gRPC shape. The TypeScript SDK validates the whole array against BatchReplaceOrdersSchema first, so a field its schema covers — selfTradePreventionMode among them — throws ValidationError before the request is sent and no sibling item executes at all. A batch whose items all fail validation returns 2xx with every result failed and no engine round trip. Only an empty or oversize (>100 items) batch still rejects the entire call with a top-level REST 400 / gRPC InvalidArgument. Earlier releases rejected the whole call for any one malformed item; if you relied on 400 meaning “nothing happened”, read results[] per item instead — a 2xx batch can contain both executed replacements and failed items.
Because a failed-to-place item’s original is already cancelled, refresh your live open orders before retrying — do not assume the original is still resting, or you may re-expose it unintentionally. A POST_ONLY_WOULD_CROSS rejection normally leaves the original resting, so it can be retried by re-pricing rather than re-placing — but the cases above are exceptions, so confirm against your open orders rather than relying on the code alone. For every other per-item failure code, treat the original as gone.
A batch replace is capped at 100 items, the same client-side and API 400 cap as Batch Create; split larger sets across requests.
Response Shapes
Order placement returns a standard response with optional immediate execution details.matchResult immediately. Resting orders may be accepted with no immediate fills.
Error Handling
- Delegated-agent policy is revoked, expired, inactive, missing, or narrower than the requested action.
- Order already filled, canceled, expired, or rejected.
- Replacement total quantity is at or below the already-filled quantity — nothing would be left to rest. This applies to margin reduce-only orders too.
- A margin reduce-only replacement whose remainder (
total - filled) exceeds the live position. - Price or quantity is not a positive decimal string.
- Entry-attached
takeProfit/stopLosson a spot or reduce-only order is rejected — they are margin entry-order only. - A normal margin order is missing
leverage, orpositionSidedisagrees withside. - A market order fills partially inside the market’s price band (1,000 bps by default) and cancels the remainder, or is rejected as insufficient liquidity or market unavailable.
- A perp order that takes liquidity (market or limit) stops at the first resting order outside the perp price band; a limit remainder that would then cross the book is cancelled instead of resting — see Perp price band.
- Perps risk simulation rejects the requested leverage or collateral impact.
- Reduce-only order would increase or flip exposure.
- A
postOnlyorder or replacement would cross the book — rejected with REST400/ gRPCInvalidArgumentcarrying the structured codePOST_ONLY_WOULD_CROSS(REST error-envelopecode, gRPCErrorInfo.reason) — adjust price and resubmit; see Post-Only Orders for the crossing predicate and the replace/batch-replace resting-order contract. postOnlycombined with aMARKETorder orIOC/FOKtime in force is rejected.- A
CROSSorder that is not reduce-only on an isolated-only market (isolatedOnly: true):400/ gRPCInvalidArgumentwith codeCROSS_MARGIN_NOT_ALLOWED. UsemarginMode: "ISOLATED"; reduce-onlyCROSSorders still pass. - A batch create, replace, or cancel exceeds 100 items — rejected client-side and with API
400(At most 100 orders per batch request). - Cancel-all matches more than 20,000 active orders — REST
400/ gRPCInvalidArgument, with nothing cancelled. Narrow by trading pair or cancel explicit batches. - The sequencer is overloaded, or the account already has its share of the engine’s queue waiting — order create, replace, cancel, and batch submits can return a retryable HTTP
503with error codeOVERLOADEDinstead of queueing (cancels keep a reserved share of the engine’s capacity, so they are refused last); back off and retry (seeretryable). - The account’s order rate budget is spent — REST
429/ gRPCResourceExhaustedwith codeRATE_LIMIT_EXCEEDED, aRetry-Afterheader, anddetails.retryAfterin seconds. The request — a whole batch included — was rejected before admission and nothing was applied, so wait out the interval and resubmit; cancels draw a separate, larger budget and cancel-all costs one item per call. Authenticated reads such asgetOrderandgetPaginatedOrdersdraw their own per-account read budget with the same429contract and no budget headers, so reconcile on theordersWebSocket stream rather than polling. See Rate Limits. - Conditional order has already triggered, expired, or been cancelled.
CANCELLED, its terminalReason gives the stable cause — USER_REQUESTED, REPLACED, LIQUIDATION, INSUFFICIENT_MARGIN, REDUCE_ONLY_EXHAUSTED, POSITION_CLOSED, OCO_SIBLING_TRIGGERED, SELF_TRADE_PREVENTION, AUTO_DELEVERAGING, PRICE_BAND, SLIPPAGE_TOLERANCE, or SYSTEM — on both the REST order response and the order WebSocket event. AUTO_DELEVERAGING means auto-deleveraging closed the position your risk bucket held and this resting order was cancelled with it; you were deleveraged as a counterparty, not liquidated, which is why it is not reported as LIQUIDATION. PRICE_BAND means the exchange’s protective price band stopped your order and its remainder was cancelled; SLIPPAGE_TOLERANCE means your own slippageToleranceBps did, so widening it can fill more, while the exchange band cannot be widened.
Order state versions
Every order response carriesversion, an opaque, compare-only revision counter for that order’s state: the sequencer step that wrote it. A strictly higher value is strictly newer state for that order. getOrder and getPaginatedOrders return the same version for the same order state, and it never advances without a state change.
The same counter rides every order WebSocket event and the orders snapshot frame, which is what makes a reconnect reconcilable without pausing the stream: keep applying events as they arrive, and merge a REST snapshot per order, taking the snapshot when snapshot.version >= local.version. See the market-maker runbook for the full recipe.
Read nothing else into the number:
- Absent means unknown, not zero — an order last written before the field existed reports no
version. Compare fields for those rows instead of assuming they are the oldest. - Gaps are normal. It comes from one global sequence shared by every order, so an order’s successive versions are sparse and consecutive values are never implied.
- Not comparable across orders. Two different orders’ versions say nothing about each other, only about each order against its own earlier reads.
updatedAt for reconciliation. updatedAt is not a single clock: order detail reports the matching-engine event time while order lists report the database commit time, so the two endpoints report different values for the same order, the commit clock can move without a state change, and timestamp precision varies per call.
Selected persisted/read-model timestamps render at fixed microsecond width with a Z suffix — 2026-09-05T01:23:45.123456Z. This covers ordinary-order createdAt, updatedAt, and expirationDate; every conditional-order timestamp; application order-view createdAt / updatedAt; and persisted TWAP window and lifecycle stamps. Ordinary-order lifecycle stamps (cancelledAt, filledAt, expiredAt, submittedAt, and acknowledgedAt) retain variable fractional width. CreateTwapOrderResponse.firstSliceAt and CreateTwapOrderResponse.endTime also retain their separate rendering. Fixed width does not make updatedAt comparable across endpoints; use version for that.
Order Patterns
Try FOK, fallback to IOC
Cancel stale intent before quoting
Quick exit with IOC
Useful Surfaces
TypeScript Trades
SDK methods for placing, replacing, cancelling, batching, and listing orders.
Orders API
REST endpoints for creating, replacing, canceling, and querying orders.
Orderbook API
Snapshot orderbook depth for a trading pair.
WebSocket API
Live order, orderbook, trade, balance, movement, and position streams.

