Skip to main content

Monaco Protocol SDK v1.0.54

This release covers the consumer-facing changes since v1.0.53. No wire field changes shape, but there is one source-level compatibility break: totalTradeCountLtd is retyped from string to integer in the generated SDKs, which stops typed callers that treat it as a string from compiling. Two further items need attention before you upgrade — keyed order submission gains two retryable error codes, and the order endpoints now declare a per-account rate limit whose budget keys you can read today. Alongside those, your own resting quote no longer hides third-party liquidity from your taker, every WebSocket gap now closes the connection with the same resync signal, withdrawals commit the authenticated wallet as the on-chain owner, and portfolio analytics move to a dedicated reader.

Breaking

totalTradeCountLtd is retyped from string to integer

totalTradeCountLtd is corrected to integer / int64 for GET /api/v1/market/pairs/{tradingPairId}/metadata, GET /api/v1/market/screener, and GET /api/v1/market/stats. The server always emitted a JSON number; the spec incorrectly declared a string, so runtime responses and requests are unchanged. These three endpoints require no authentication. This is a source-level break for typed callers, even though the wire is unchanged. @0xmonaco/types now declares totalTradeCountLtd?: number | null where it previously declared string | null, and the generated Rust REST SDK produces an integer model field where it previously produced a String. Only string-specific uses break: parseInt(...), calling a String method such as .startsWith() or reading .length, assigning to a string-typed field, or passing it to a parameter declared string. Replace those with direct numeric use. Generic conversion and formatting keep compiling untouched — Number(...), String(...), .toLocaleString() and template interpolation all accept a number — so there is no need to strip them out. Untyped JavaScript and raw REST callers are unaffected, because they were already receiving a number.

Changed

Keyed order submission reports capacity and availability separately

POST /api/v1/orders and POST /api/v1/orders/batch-create (OrdersService.CreateOrder / BatchCreateOrders) expose two structured codes for the optional idempotencyKey. Both are retryable, both map to HTTP 503 / gRPC UNAVAILABLE, and in a batch each arrives on the failing item’s error field rather than failing the whole call.
  • 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; only a new key that needs another receipt is refused. The refusal does not accept or reserve the key, so nothing was applied. Capacity frees as protected receipts finish acknowledgement and persistence and become evictable — usually seconds, not the 24-hour retention window — so retry the same key with bounded backoff.
  • ORDER_SUBMISSION_UNAVAILABLE means the original response for a retained key cannot currently be retrieved, or keyed admission is busy. Unlike a capacity refusal it is not evidence the order was rejected — a submission under that key may already exist and may already have executed. It never authorizes duplicate execution, so retry the same key with the same normalized payload and let the replay report the outcome.
The response cache behind these keys is now bounded and evictable, so a full cache no longer refuses new keyed submissions on its own — durable responses can be evicted while exact key history stays retained. Replay preserves the original order identity and never executes a second order. The 24-hour retry boundary is unchanged and applies only to previously accepted keys; a different payload on a retained key still returns IDEMPOTENCY_KEY_CONFLICT (HTTP 409 / gRPC ALREADY_EXISTS). See Error Handling and Safe Retries with Idempotency Keys.

Order and movement endpoints declare a per-account rate limit

The order endpoints document a 429 rejection with code RATE_LIMIT_EXCEEDED, a Retry-After header, and details.retryAfter carrying the denying window’s real horizon — about 1 second for a spike, up to 60 seconds for sustained overflow. On gRPC the equivalent is RESOURCE_EXHAUSTED with google.rpc.RetryInfo. Budgets are keyed to the account and weighted by order items, split into two classes, named here by REST path because the SDK, REST and gRPC spellings differ. Order creation covers POST /api/v1/orders, the replace endpoints, and the batch create/replace items without reduceOnly. Risk reduction, at twice the creation budget, covers every cancel — /orders/cancel, /orders/batch-cancel, and the conditional and TWAP cancels — plus any create item with reduceOnly: true. Each class enforces a per-second and a per-minute window and a request must fit both; an over-budget batch is rejected as a whole request, never per item, and a mixed batch charges each class for its own items. Mass exit — POST /api/v1/orders/batch-cancel-all and its by-pair form — is never rate limited. Withdrawal initiation and the margin collateral transfers share a separate per-user movement budget. Successful gated order responses return the live budget. The keys are emitted lowercase — x-ratelimit-limit-second, x-ratelimit-remaining-second, x-ratelimit-limit-minute, x-ratelimit-remaining-minute — and serve both transports. Over REST you may look them up in any case, since HTTP header names are case-insensitive; over gRPC they arrive as response metadata and must be looked up lowercase. Movement endpoints return no budget keys. Delegated agent sessions draw their owner’s budget, sub-accounts have their own, and PrivateLink connections draw their own 4x budgets. Enforcement is being rolled out per environment. Until it is enabled where you trade, an over-budget request is admitted and only measured — the budgets and headers above are already returned, so build back-off handling against them now. See Rate Limits.

balance_update carries tokenSymbol on margin-collateral frames

The balance_update WebSocket event now reports tokenSymbol on margin-collateral frames — reasons trade, fee, rebate, funding, liquidation, and adl. These could previously arrive with tokenSymbol: null, leaving tokenAddress as the only identifier, for a margin account whose collateral symbol was not recoverable after a matching-engine restart. A collateral transfer now binds the symbol it carries rather than only the balance id and token address, and the emitter resolves a still-missing symbol from the configured asset registry by the account’s collateral token. The symbol is therefore present whenever the token is a configured asset, regardless of per-account binding state. tokenSymbol remains typed string | null and is null only when the account has no bound collateral token, or the token is not a configured asset. This is wire-compatible: symbol-keyed clients see a value where they previously saw null. See Balance Updates.

Maker fill events carry postOnly

OrderFilled and OrderPartiallyFilled with role: "maker" now carry postOnly: true when the resting order was admitted post-only. This was the one order-event variant without the flag, and the one that matters most: an admitted post-only order can never fill as taker, so its fills are exactly the frames that previously omitted it. The flag follows the same absent-rather-than-false contract as the acknowledgement, taker fills, and the cancellation and expiry events, so data.postOnly ?? false now reads correctly on maker fills. It is denormalized onto the trade at fill time, so it survives the fill that fully retires the order. The change is additive and wire-compatible — postOnly was already declared optional on the shared order-event type, so no TypeScript shapes change. A maker fill emitted by a server predating this change still omits it. See Post-Only on Order Events.

Fixed

An own resting quote no longer hides third-party liquidity

Order submission through sdk.trading.placeLimitOrder / placeMarketOrder and authenticated POST /api/v1/orders (OrdersService.CreateOrder) now counts only non-self liquidity during the early self-trade check. An own resting quote previously consumed that check’s depth budget, so a third-party order resting behind yours — including at the same price — was invisible to an incoming taker and the request was rejected before reaching the book. Your own quotes remain untouched. The sequenced engine check is still authoritative: if the matching walk encounters an own order and eligible third-party depth cannot cover the whole request, SELF_TRADE_NOT_ALLOWED still rejects before any fills. Price limits, market-order slippage limits, and margin admission still apply. Request and response fields are unchanged, and no selectable self-trade prevention mode is added. See Self-Trade Prevention.

Every WebSocket gap closes the connection with the resync signal

A detected broadcast gap now closes the whole connection with the existing 1013 resync-required signal on every subscription mix, including sockets holding only positions, balances, account, instruments, orderbook, ohlcv, or market-stat subscriptions. The close frame is the machine-readable gap signal; no new JSON error protocol is introduced and no history is replayed automatically. With automatic reconnect enabled the SDK opens a new socket and sends its authentication and subscription requests before invoking onResync, without waiting for acknowledgements or snapshots. Applications still own REST reconciliation, especially for the channels that have no subscribe-time snapshot. See Slow-client disconnect.

Master withdrawals commit the authenticated wallet as on-chain owner

Authenticated master withdrawals through POST /api/v1/withdrawals and WithdrawalsService.InitiateWithdrawal now commit the authenticated wallet as the on-chain withdrawal owner. The commitment previously carried a zero owner, so a payout whose destination rejected it could not be recovered by anyone. Request and response fields are unchanged. Owners can use the vault’s owner-authorized redirection to recover a future withdrawal whose original destination rejects payment. This is not retroactive: historical withdrawals retain their original commitments and replay behavior.

Collateral transfers emit a balance update when either component parses

A successful authenticated margin account collateral transfer now emits the balance_update WebSocket event when either matching-engine balance component parses. The event overlays each valid availableRaw or lockedRaw value onto the prior wallet balance and recomputes the total; an invalid component retains its prior value. No event is emitted when neither component parses, or when a risk bucket allocation moves no wallet balance. Requests and responses are unchanged.

Portfolio analytics read from a dedicated reader

sdk.portfolio.getSummary(), getPerformance(), getRealizedPnl(), getAssets(), and the sdk.profile aggregate stats and chart methods now use a dedicated read replica and may reflect replica lag. A reader outage returns HTTP 503 / gRPC Unavailable rather than moving analytics onto the writer. Request fields and successful response fields are unchanged. sdk.portfolio.getMargin() and trade history keep their existing freshness requirements: they are writer-backed rather than replica-backed. Note that getMargin()’s withdrawal bounds are not unconditionally live, and the fallback has two scopes. If the matching engine serves no bounds at all, the account total and every risk bucket row fall back to a persisted reconstruction. If the engine serves bounds but one bucket is missing from them, only that row falls back while the rest stay live. The reconstruction is pinned to the same formulas the withdrawal gates use, but it cannot see the gates’ resting-order reserves or the parent margin account’s live allocation constraint, so treat a fallback value as an estimate rather than an enforceable bound. See Portfolio Analytics. The affected REST endpoints are GET /api/v1/accounts/me/portfolio and its /chart, /summary, /performance, /realized-pnl, and /assets paths; the matching gRPC methods are on AccountsService.

Upgrade

Before upgrading, treat ORDER_SUBMISSION_CAPACITY_EXCEEDED and ORDER_SUBMISSION_UNAVAILABLE as retryable rather than terminal, and retry them with the original key and normalized payload. Start reading the X-RateLimit-* budget headers while enforcement is still rolling out, and make sure a batch submitter can handle a whole-request rejection rather than a per-item one. Update string-specific handling of totalTradeCountLtd — a parseInt, a String method call, or a string-typed assignment will not compile until you read it as a number. Generic formatting such as String(...) or template interpolation needs no change. If you display a portfolio number next to a live one, expect the analytics side to lag slightly and to return 503 during a reader outage.