Skip to main content
Build trading experiences on Monaco by choosing the product surface your application needs. Spot and perps share market data, order entry, order management, live streams, and trading pair concepts, but they use different funding and risk models.

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

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 to GTC.
Use limit orders when the user cares about price protection or wants to provide liquidity.

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.
Use 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).
Use 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 an idempotencyKey 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.
Keys contain 1–64 characters from 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

A clientOrderId 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 — a MARKET order, an IOC or FOK, or a LIMIT that 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-a can follow your quote across every replace.
For a legacy unkeyed submission, do not resubmit on a lost response. clientOrderId is a correlation handle, not an idempotency key, and resubmitting is unsafe for any order that takes liquidity.If a create’s response is lost and you resubmit the same request with the same clientOrderId:
  • If the first attempt rested, and is still resting, you get 409 CLIENT_ORDER_ID_CONFLICT naming the order that holds it.
  • If the first attempt executed — a MARKET order, an IOC/FOK, or a marketable LIMIT that filled — it never took the handle, so the resubmission is accepted as a new order and can execute again. A success response does not mean the first attempt failed.
  • If the first attempt rested and has since filled or been cancelled, the handle is already free, so the resubmission is likewise accepted as a new order and can execute again.
Treat a lost response as an unknown outcome and reconcile it before acting.

Recovering from a lost response

Subscribe before you place. The stream is not replayable: a slow-client disconnect (close code 1013) 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 clientOrderId conflict, a plain MARKET submit 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 LIMIT with IOC or FOK that cannot fill on arrival, or a MARKET order whose apparent liquidity is invalidated by maker-risk revalidation after it was accepted. The request succeeds with a 2xx, matchResult.status is REJECTED, and the stream emits OrderPlaced then OrderRejected, both carrying your clientOrderId.
A success response does not mean the order executed. An IOC or FOK that cannot fill — or a MARKET order that loses its apparent liquidity to maker-risk revalidation — is accepted and then rejected, so it returns a 2xx with nothing traded. Read matchResult.status rather than inferring the outcome from the HTTP status.
How to read silence. You cannot. A matching event is positive proof the order was accepted, but silence is never proof of the opposite — and there is no client-observable condition that makes it so. The fan-out is non-persistent Core NATS, and the service can re-establish an ended upstream subscription without closing your socket, so events can be missed while your connection still looks perfectly healthy. Treat a bounded wait that returns nothing as unresolved, not as a negative answer. When the stream cannot answer, check whether your position or balance moved. For a taking order that is the economic truth, and because it does not depend on the order read model it survives replica lag. Do not reconcile by polling the order list. Order history is written asynchronously and served from a read replica, so immediately after a create it can report “not found” for an order that did land — most likely exactly when you need it, since a timeout usually means the system is busy. A missing row is not evidence. Note also that “rejected” splits here: an order rejected before acceptance has no row at all, while one rejected after acceptance is persisted with status 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.
It is not an idempotency key. Monaco does not return the original order on a duplicate — where the guard fires at all, it rejects the duplicate. It also does not compare the rest of the payload: a resubmission with the same clientOrderId but a different price or quantity is rejected just the same, because the handle is what is taken, regardless of what else changed.Reuse after the original is terminal is legal and expected, so a lookup by a value you have reused across many orders is ambiguous — the guarantee is only that at most one resting order holds it at any instant.
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; explicit marginAccountId 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 a marginAccountId. Use parent simulation when the order should draw from the user’s general parent margin collateral:
Use risk-bucket simulation when the order targets a specific market bucket or strategy key. This endpoint (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.
Run risk simulation on debounced order-form changes, surface 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).
To check whether a target clears its own costs before you set it, preview the close as a reduce-only MARKET order of the position’s size with 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:
Pass 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-quantity leg — 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.
Alternatively, attach TP/SL to the entry order itself: pass 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 by trailBps.
  • 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.
Attach one to an open position, alone or alongside fixed legs:
Or on the entry order (margin, not reduce-only):
Both return 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. A MARKET 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 TRIGGERED conditional order (getConditionalOrder and listConditionalOrders) carries triggeredOrder — the close’s current status, filledQuantity, averageFillPrice, totalTakerFees, filledAt and terminalReason, read from that order at request time. Compare triggeredOrder.averageFillPrice with triggerPrice to 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 while getOrder is cache-first, persistor lag can also leave them showing different states — read getOrder for the close’s very latest. The field is absent until the trigger fires, and is not carried on the conditional_orders WebSocket frames.
  • The close itself carries conditionalOrderId in 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.
To follow TP/SL all the way through execution, subscribe to both conditional-order events and the ordinary execution/state channels. The conditional event confirms the trigger. The triggered close then publishes through the same orderbook, order, trade, OHLCV, position, and balance paths as a directly submitted order, so those channels carry the fill and resulting state rather than requiring a manual refresh.

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

Canceling an open order releases locked balance and removes the order from the book. It cannot be undone. Pass 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.
The target means “leave this much resting”, so a retried request converges instead of compounding. It must be a positive multiple of the market’s quantity step and strictly below the order’s current remaining quantity — emptying an order is a full cancel’s job, and growing one is a replace. The reduction emits a non-terminal 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

Batch cancels are best-effort per order. Some cancellations can fail while others succeed, so inspect individual results before updating UI state.

Cancel All

Use cancel-all actions for emergency exits, cleanup flows, or agents that need to clear stale intent before submitting new quotes. Cancel-all is capped at 20,000 matching active orders. Above that cap, it cancels nothing and returns REST 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.
You can update price, quantity, or both. Filled, canceled, expired, or rejected orders cannot be replaced. 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

Each order is processed independently. A failure in one item does not block the rest of the batch. Each item’s failure — including a 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

Each successful replacement returns a new order ID. Batch replace is cancel-first: the matching engine cancels every item’s original before placing any replacement, in request order, so a two-sided quote can shift across its own resting prices in one call without meeting self-trade prevention at all, and a replacement can lock funds freed by any original in the batch. That ordering matters more under the mode dispatch than it did under the old whole-order rejection: were the originals still resting, a replacement crossing one of them would cancel it under the default 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_CROSS while a surviving sibling occurrence still cancels the shared original (first occurrence wins, as always for duplicates).
The fourth goes the other way — the item is refused but nothing of yours is cancelled:
  • 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.
Treat the guarantee below as “a refused post-only item normally keeps its original”, and confirm from your open orders rather than assuming. Pre-sequencing validation failures are scoped to their item, the same contract as Batch Create: a malformed 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.
Market orders and aggressive limit orders can return matchResult immediately. Resting orders may be accepted with no immediate fills.

Error Handling

Common cases to handle:
  • 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/stopLoss on a spot or reduce-only order is rejected — they are margin entry-order only.
  • A normal margin order is missing leverage, or positionSide disagrees with side.
  • 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 postOnly order or replacement would cross the book — rejected with REST 400 / gRPC InvalidArgument carrying the structured code POST_ONLY_WOULD_CROSS (REST error-envelope code, gRPC ErrorInfo.reason) — adjust price and resubmit; see Post-Only Orders for the crossing predicate and the replace/batch-replace resting-order contract.
  • postOnly combined with a MARKET order or IOC/FOK time in force is rejected.
  • A CROSS order that is not reduce-only on an isolated-only market (isolatedOnly: true): 400 / gRPC InvalidArgument with code CROSS_MARGIN_NOT_ALLOWED. Use marginMode: "ISOLATED"; reduce-only CROSS orders 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 / gRPC InvalidArgument, 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 503 with error code OVERLOADED instead of queueing (cancels keep a reserved share of the engine’s capacity, so they are refused last); back off and retry (see retryable).
  • The account’s order rate budget is spent — REST 429 / gRPC ResourceExhausted with code RATE_LIMIT_EXCEEDED, a Retry-After header, and details.retryAfter in 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 as getOrder and getPaginatedOrders draw their own per-account read budget with the same 429 contract and no budget headers, so reconcile on the orders WebSocket stream rather than polling. See Rate Limits.
  • Conditional order has already triggered, expired, or been cancelled.
When an order ends in 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 carries version, 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.
Equal versions mean different things on the two surfaces, and the difference decides whether you write > or >=.Between two REST reads, equal means identical state — a REST row is only ever a step’s final state.On the stream, equal means only same step. One step emits several events for one order (an OrderPlaced and then its fills), and an unkeyed batch place or a batch replace shares one step across every item. A create batch containing any idempotencyKey processes items individually, so its items need not share a version. So when a snapshot row ties with local state, the snapshot is the more complete view and must win: merge with >=, never >.
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.
Prefer it over 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.