Skip to main content

Monaco Protocol SDK v1.0.41

This release adds a client-assigned clientOrderId correlation handle across order placement, replacement, and every order read and WebSocket event; gives order-attached take-profit / stop-loss legs their own sizing so they no longer freeze at the entry order’s quantity; adds a new authenticated account WebSocket channel streaming a margin account’s live health; and emits a movements ledger entry for on-chain deposits and withdrawals so that channel lines up with balances. It also fixes — and corrects the 1.0.40 migration guidance for — funding an isolated risk bucket, which was silently ratcheting a sole open position’s stored margin. One thing to act on: if you followed the 1.0.40 note and used transferCollateralToRiskBucket to increase a position’s margin, that is not what it does — see the fix below.

Added

clientOrderId — a correlation handle on order placement

placeLimitOrder, placeMarketOrder, replaceOrder, batchCreate and batchReplace (@0xmonaco/core; POST /api/v1/orders, PUT /api/v1/orders/{orderId}, POST /api/v1/orders/batch-create and .../batch-replace; gRPC CreateOrderRequest, ReplaceOrderRequest, BatchCreateOrderItem, BatchReplaceOrderItem; the place_limit_order / place_market_order / replace_order MCP tools) now accept an optional clientOrderId — a label you choose that Monaco stores and echoes back. It is up to 64 characters from A-Za-z0-9._:-; surrounding whitespace is trimmed and a blank string is treated as absent. A longer value, or one containing any other character, is rejected with HTTP 400 / gRPC INVALID_ARGUMENT; on batch-create the rejection is per item with error.code INVALID_CLIENT_ORDER_ID, while batch-replace validates every item up front and rejects the whole request. The handle is echoed on order detail and order list responses (clientOrderId, absent when the order carried none) and on every orders WebSocket event — placed, taker fill, maker fill, cancelled, rejected and expired. A maker fill carries the resting order’s own handle, so a filled quote identifies itself without a follow-up read, and a submit whose HTTP response was lost can be resolved from the stream alone. It is a correlation handle, not an idempotency key: Monaco does not de-duplicate on it and does not enforce uniqueness, so sending the same value twice places two orders. It does not make a lost-response retry safe — reconcile against the order list first, matching on clientOrderId, and resubmit only if nothing landed. A replacement is a new order with a new id: it carries its own clientOrderId and inherits nothing from the original, the same way postOnly already behaves. Orders placed before this release report no clientOrderId.
Corrections since this release. Three statements above have been superseded. The original wording is left in place; each correction is listed here:
  • “does not enforce uniqueness” — v1.0.42 added a uniqueness guard, but only among your resting orders. An order that never rests takes no handle.
  • “reconcile against the order list” — the order list is served asynchronously from a replica and can omit an order that did land, and an order rejected before acceptance is never written to it at all. Reconcile on the order WebSocket stream (sdk.ws.userOrders) instead. See Client Order IDs.
  • “on every orders WebSocket event — placed, taker fill, maker fill, cancelled, rejected and expired” — accurate for one class of rejection only. An order rejected after acceptance — a LIMIT with IOC/FOK that cannot fill, or a MARKET order whose apparent liquidity is invalidated by maker-risk revalidation — returns 2xx with matchResult.status REJECTED and does emit OrderRejected. A create rejected before acceptance — malformed, post-only would cross, handle conflict, or a plain MARKET submit with no liquidity, rejected during pricing — fails the request, emits nothing, and is never written to order history either (an order rejected after acceptance is persisted with status REJECTED). And the retained claim above that a lost response “can be resolved from the stream alone” is retracted: a matching event is positive proof the order landed, but the stream is not replayable and its fan-out is non-persistent, so silence is never evidence it did not — absent a matching event the outcome is unresolved.

Order-attached take-profit / stop-loss legs size independently

The takeProfit / stopLoss legs attached to an order at placement (ParentTpSlLeg; placeLimitOrder / placeMarketOrder / batchCreate in @0xmonaco/core, the matching REST and gRPC order requests) gain quantity and closePosition, the same sizing contract the position-attached TpSlLeg already carries. Previously an order-attached leg had no way to express its own size and was frozen at the parent order’s quantity, so a position that later grew past it kept a stop covering only part of what was held while still reading as attached. Omit both fields (or set closePosition: true) to resolve the full live position size at trigger time, or name a quantity for a deliberate partial that never resizes; the two are mutually exclusive, and quantity is required when closePosition is false.

account WebSocket channel — live margin-account health

A new authenticated channel — subscribe with account — carries a margin account’s health: equity, collateral, free collateral, margin ratio, and maintenance requirement (all decimal strings), plus the margin account id and update timestamp, under an account_update event. The channel routing and the AccountEvent type in @0xmonaco/types ship in this release so integrators can build against the contract, but the server-side producer that emits these frames is gated off by default and not yet enabled in the deployed environments — subscribing succeeds and the wire shape is stable, but no frames arrive yet. When the producer is enabled its lag policy is conflate (a slow subscriber is fast-forwarded to the latest state) and it republishes an unchanged frame as a keepalive after a bounded quiet interval, so treat repeats as keepalives and each frame as a terminal-state snapshot per margin account, not an additive delta. There is no sdk.ws helper, so subscribe over the raw WebSocket protocol with a Subscribe frame naming the account channel; the raw wire is snake_case (event_type, user_id, margin_account_id, free_collateral, margin_ratio, maintenance_requirement, updated_at) while the AccountEvent type is the camelCased ergonomic shape nothing converts to on this channel. See the WebSocket reference for the exact wire frame.

movements frames for on-chain deposits and withdrawals

On-chain deposits and withdrawals now emit a user_movement frame on the movements channel, co-ordered with their balance_update on the balances channel. Every deposit/withdrawal balance change now has a matching movements ledger entry — transaction type deposit or withdrawal, carrying the amount, token, symbol, and balance snapshot — so the two streams line up; previously a deposit or withdrawal produced a balances frame with no corresponding movements entry. The UserMovement wire shape is unchanged (transactionType is a free-form lowercase string); these are additional transaction types on the existing channel.

Fixed

Funding an isolated risk bucket no longer ratchets a sole position’s margin

Funding an isolated risk bucket through transferCollateralToRiskBucket() (@0xmonaco/core; the transfer_collateral_to_risk_bucket MCP tool; REST POST /api/v1/margin/risk-buckets/collateral/transfer-in; gRPC TransferCollateralToRiskBucket) no longer credits the bucket’s sole open position’s stored margin. It now always allocates reusable bucket collateral, as documented. Previously, when the bucket held exactly one open position, every transfer was silently treated as Adjust Margin, so routine bucket funding permanently ratcheted the position’s initial margin and collateral requirement instead of leaving the collateral redeployable. This corrects the 1.0.40 migration note, which presented transferCollateralToRiskBucket as the addPositionMargin replacement. It is not: it funds reusable bucket collateral. To explicitly increase a sole open position’s stored margin, use the pair-scoped legacy transfer transferCollateralToMarginAccount(parentMarginAccountId, { asset, amount, tradingPairId }) (@0xmonaco/core; REST POST /api/v1/margin/accounts/{marginAccountId}/collateral/transfer-in with tradingPairId; gRPC TransferCollateralToMarginAccount). There is no MCP tool for this — the pair-scoped transfer is deliberately not exposed — so an assistant increases a position’s stored margin through the REST endpoint or @0xmonaco/core.

Upgrade

The MCP server ships at 1.0.41 too; it runs via npx -y @0xmonaco/mcp-server, so restarting your assistant picks up the new version, the clientOrderId option on the order tools, and the corrected transfer_collateral_to_risk_bucket behavior. Nothing in this release is breaking. The one thing worth acting on: if you followed the 1.0.40 migration and used transferCollateralToRiskBucket to add margin to a position, switch to transferCollateralToMarginAccount(parentMarginAccountId, { asset, amount, tradingPairId }) — the risk-bucket transfer funds reusable collateral and no longer ratchets a sole position’s stored margin.