Skip to main content

Monaco Protocol SDK v1.0.40

This is a large release. The headline is a collateral-model change: the two position-margin methods that sourced collateral from the spot wallet are removed, and adjusting a position’s margin now flows through its risk bucket and the parent margin account. Alongside it, the WebSocket surface is brought fully in line with the wire — a breaking casing change on the positions channel, a locked cross-language drift gate, new market_stats channels, and richer balance_update / movement / position events — the market-data endpoints gain dollarized open-interest and volume figures, and the candlestick endpoint finally honors its own bounds. Two things to act on before upgrading: migrate off addPositionMargin / reducePositionMargin, and update any WebSocket position consumer that matches on the PascalCase Open status.

Breaking

addPositionMargin / reducePositionMargin removed — adjust margin through the risk bucket

positions.addPositionMargin() and positions.reducePositionMargin() (@0xmonaco/core), the perpRoutes.positions.addMargin / reduceMargin route helpers, usePositions()’s addPositionMargin / reducePositionMargin (@0xmonaco/react), the add_position_margin / reduce_position_margin MCP tools, the REST endpoints POST /api/v1/positions/{position_id}/margin/add and .../margin/reduce, and the gRPC PositionsService.AddPositionMargin / ReducePositionMargin RPCs are all removed, along with the AddPositionMarginRequest / ReducePositionMarginRequest / PositionMarginResponse types and their validation schemas. Both sourced collateral from the spot wallet, so collateral already held in the parent margin account could not be deployed to a position at all, and releasing margin pushed it out of the margin account entirely rather than back to the parent. Migration. Adjust a position’s margin through its risk bucket, moving collateral between the bucket and the parent margin account — neither leg touches the spot wallet. Fund the parent first if it has no free collateral.
  • Allocate — transferCollateralToRiskBucket({ asset, amount, tradingPairId, marginMode: "ISOLATED" })
  • Release — transferCollateralFromMarginAccount(parentMarginAccountId, { asset, amount, tradingPairId })

WebSocket position status is now SCREAMING_SNAKE_CASE

Position events on the positions channel now carry status as OPEN, LIQUIDATING (a closing position), CLOSED, LIQUIDATED, or EXPIRED — matching the REST position status strings and the published @0xmonaco/types contract exactly — instead of the previous PascalCase (Open), which diverged from REST for the same field. PositionEventStatus has been narrowed accordingly. A consumer that matched on "Open" must switch to "OPEN". The position event data also now carries optional tradeId and orderId, referencing the trade and/or order that caused the update (omitted when not applicable).

Added

marginMode on collateral transfer-out — releasing from a cross bucket

marginAccounts.transferCollateralFromMarginAccount() (@0xmonaco/core; TransferCollateralRequest in @0xmonaco/types; REST POST /api/v1/margin/accounts/{margin_account_id}/collateral/transfer-out; gRPC TransferCollateralFromMarginAccountRequest.margin_mode) accepts an optional marginMode. A cross risk bucket has no trading pair, so tradingPairId could not name it and there was no way to address a cross bucket on a transfer-out at all — collateral could enter a cross bucket and never leave. marginMode: "CROSS" selects the cross bucket, and the release lands in the parent margin account like every other bucket release. The two selectors are mutually exclusive: marginMode: "CROSS" together with tradingPairId is rejected client-side.

Dollarized open interest and 24h volume on market-data endpoints

getPerpMarketSummary now returns openInterestBase, openInterestNotional, and quoteVolume24h, and getMarketMetadata now returns quoteVolume24h (gRPC GetPerpMarketSummaryResponse / GetMarketMetadataResponse). The perp summary previously exposed only base-asset openInterest and base-asset volume24h, so labelling either as USDC was wrong by roughly the mark price. openInterestNotional is openInterestBase × mark price (falling back to the latest close), and quoteVolume24h is the dollar figure directly; openInterest remains a base-asset alias of openInterestBase. getScreener (GET /api/v1/market/screener) additionally returns openInterestBase and openInterestNotional on every ScreenerItem, so a client can read all pairs’ open interest in one call instead of a per-pair getPerpMarketSummary fan-out. Both are null for spot pairs, and a margin pair with no open positions reports "0". The screener notional is valued at the pair’s latest trade price (a snapshot); the mark-accurate notional lives on getPerpMarketSummary / getOpenInterest.

market_stats WebSocket channels

A new public per-market channel — subscribe with market_stats:<tradingPairId> — delivers periodic per-market frames: index / mark / mid prices, best bid/ask, open interest and its limit, last trade price, 24h volume / high / low and both absolute and percentage price change, and funding state (current rate, estimated next rate, next funding timestamp, premium, and clamp bounds). A companion all-markets summary channel — subscribe with market_stats (or market_stats:*) — delivers, on a slower cadence, a single frame carrying a reduced per-market summary for every perp market: trading pair id, symbol, last trade price, mark price, absolute and percentage 24h price change, 24h base/quote volume and high/low, open interest, current funding rate, next funding timestamp, and a market_status label (TRADABLE, POST_ONLY, HALTED, NOT_READY, or UNKNOWN). Both channels are public — no session handshake required. SPOT markets flow through both, carrying orderbook, last trade, 24h stats, and a TRADABLE / HALTED label, with the perp-only fields (index/mark price, open interest and limit, funding rate/timestamp/premium, base interest rate, funding clamps) omitted.

Richer position, balance, and movement events

  • Position events now also emit on liquidation-worker and ADL closes and for counterparties impacted by a fill: a terminal LIQUIDATED update — with reason "liquidation" for a maintenance-margin liquidation and "adl" for an auto-deleverage close — when a position is force-closed, plus live OPEN refreshes for surviving or partially-reduced positions. Funding settlements now also emit a position_update with reason: "funding" carrying the position’s post-settlement state (status OPEN, unchanged size), so a subscriber can attribute a live position change to funding.
  • The balance_update reason union gains fee, rebate, funding, margin_deposit, liquidation, and adl, so a collateral change can be attributed to a fee charged, a maker rebate credited, a funding settlement, a deposit routed into margin collateral, a liquidation, or an auto-deleverage. Perp/margin fills on the order-fill path now emit a trade-reason collateral frame for every margin account the fill touched — the resting maker(s), not only the aggressing taker — and a separate rebate frame when a maker earned a rebate (negative maker fee). Each frame carries the user’s resulting balance for one token (the envelope’s userId plus the payload’s tokenAddress — the user’s aggregate for that token, with no per-margin-account key): it is a terminal-state snapshot, not an additive delta, so apply the latest balance per (userId, tokenAddress) rather than summing the reason frames.
  • Funding settlements surface on the movements channel with transactionType: "funding", reconciling with the matching balance_update whose reason is funding.

Changed

A margin shortfall is no longer reported as an insufficient balance

InsufficientInitialMargin and InsufficientFreeCollateral previously surfaced as Insufficient balance: required=..., available=.... They now carry the risk engine’s own wording:
The HTTP status and error type are unchanged (400 / invalid input); only the message differs. Clients that match on the message string to detect a funding problem must be updated — the account’s wallet is not short, its margin is, and depositing does not fix a leverage or position-size problem. Genuine wallet shortfalls are untouched and still read Insufficient balance.

The candlestick endpoint now honors startTime, endTime, and limit

GET /api/v1/market/pairs/charts/candlestick/{tradingPairId}/{interval} (getCandlesticks) accepted all three parameters and then returned far more data than either bound allowed — a 20-minute window could return 350 bars spanning 5.8 hours, 330 of them before startTime. Two behaviors caused it, and both are gone:
  • The window is now absolute. A range holding fewer bars than limit was topped up with history from before startTime; it now comes back short instead, and no bar outside [startTime, endTime] is ever returned. Both bounds are inclusive.
  • limit is now a hard cap. Gap-filled flat bars (buckets with no trades, carrying the previous close) are now counted towards limit, and the bar the request anchored on is the one that survives the cap — a forward page (startTime) still begins at startTime, while a latest or backward page still ends at its newest bar.
Unchanged: limit still defaults to 350 and clamps to 500, bars are still ascending oldest-to-newest, and forward / backward pagination still walk from startTime / end at endTime. Clients that relied on the old padding to fill a chart should widen their requested range, or drop startTime to get the most recent limit bars.

Consistent order-event fill quantities

Order WebSocket events now report a consistent filled-quantity representation across taker, maker, and IOC/cancel events. filledQuantity is always the cumulative order fill (a maker event previously put only that trade’s quantity there); a new optional lastFillQuantity / lastFillQuantityRaw carries the incremental fill contributed by the event (the summed trades for a taker, the single trade for a maker) and is omitted when the event carries no fill. The IOC/market partial-cancel event now also carries filledQuantity / filledQuantityRaw, mirroring the fill events; its legacy totalFilled / totalFilledRaw remain as aliases. The full order-cancelled payload’s cancelledAt is now RFC3339 with a Z suffix, matching executedAt, and its typed shape now also declares averageFillPrice (already present on the wire).

Batch order requests are capped at 100 items

Batch create / replace / cancel requests are now capped at 100 items per request (Zenith audit #11). BatchCreateOrdersSchema and BatchReplaceOrdersSchema reject arrays longer than 100 with a validation error before the request is sent, and the new MAX_BATCH_ORDER_ITEMS constant (100) is exported from validation/trading. The REST and gRPC APIs reject an oversized batch create/replace/cancel with a 400 and the message At most 100 orders per batch request; the server-enumerated cancel-all endpoint is unaffected. Under sequencer overload the APIs may now return a retryable 503 with error code OVERLOADED instead of queueing requests unboundedly.

tokenSymbol populated on deposit/withdrawal balance events

The balance_update event (authenticated balances channel) now carries the registry-configured tokenSymbol (e.g. "USDC") on deposit and withdrawal events; it was previously null on those reasons, forcing clients to fall back to tokenAddress. The field type is unchanged (string | null) — null now only appears for tokens without a registry entry.

WebSocket event surface locked with a drift gate

Every MonacoWebSocket channel (orders, userOrders, orderbook, ohlcv, trades, movements, balances, conditionalOrders, positions, liquidations) now has a golden wire-format fixture and a test asserting @0xmonaco/types declares a field for every wire key the SDK surfaces, plus a coverage assertion that fails if a future channel lands with a loose handler or without a drift test. This keeps the typed event surface in sync with the Rust wire format; the visible effect this release is the movement-event fields below and the order/position event corrections above.

Fixed

WebSocket movement event parser reads the current wire

The movement event parser now reads the serialized token key (with a token_address fallback) and surfaces status, assetId, and blockNumber from the wire. Previously it read only the legacy token_address alias — absent from the current wire — and dropped those fields. UserMovementEventData now declares token / status / assetId / blockNumber (token optional; tokenAddress kept as a populated back-compat alias).

IOC position closes accept and require limitPrice

ClosePositionSchema and the close-position MCP tool now accept — and require — limitPrice on IOC close requests, matching the server contract. The server executes an IOC close as a LIMIT+IOC order and rejects it without a price, but the schema previously rejected any IOC request that carried one, making IOC closes impossible through the SDK. limitPrice is now required for LIMIT and IOC closes and forbidden only for MARKET.

Deprecated

TIME_IN_FORCE_GTD ("GTD")

GTD is declared in the protos but was never implemented, and order admission has always rejected it. Submitting timeInForce: "GTD" on create or batch-create fails with HTTP 400 / gRPC INVALID_ARGUMENT and the message Invalid timeInForce: Time in force must be GTC, IOC, or FOK — unchanged behavior; only the declaration is now marked deprecated, and the documentation that incorrectly advertised GTD as supported has been corrected to GTC. The enum value is retained so generated clients keep compiling. For an order that expires at a set time, send timeInForce: "GTC" with expirationDate — the GTC expiration job enforces it.

Upgrade

The MCP server ships at 1.0.40 too; it runs via npx -y @0xmonaco/mcp-server, so restarting your assistant picks up the new version, the removed position-margin tools, and the corrected close-position contract. The two breaking changes and the things worth acting on:
  1. Migrate off addPositionMargin / reducePositionMargin. Allocate with transferCollateralToRiskBucket({ ..., marginMode: "ISOLATED" }) and release with transferCollateralFromMarginAccount(parentMarginAccountId, { ..., tradingPairId }); both move collateral between the risk bucket and the parent margin account, never the spot wallet.
  2. Update WebSocket position consumers for the new status casing. Match on OPEN / LIQUIDATING / CLOSED / LIQUIDATED / EXPIRED, not the old PascalCase Open.
  3. Re-check margin-shortfall handling. Match on the new insufficient initial margin / insufficient free collateral messages, not Insufficient balance, when detecting a margin (not wallet) shortfall.
  4. Size batch requests to 100 items and handle a retryable 503 OVERLOADED, and pass limitPrice on IOC position closes.