> ## Documentation Index
> Fetch the complete documentation index at: https://docs.0xmonaco.com/llms.txt
> Use this file to discover all available pages before exploring further.

# V1.0.56

# Monaco Protocol SDK v1.0.56

This release covers the consumer-facing changes since v1.0.55. Two throttles tightened, and both can reject a client that worked before: `reduceOnly` orders no longer draw the larger risk-reduction budget, mass exit and TWAP creation are now metered per call, sub-accounts now share a budget with their master account, and every WebSocket connection now has an inbound message cap that closes the socket when it is exceeded. Alongside those, a new public endpoint lists the chains and assets the sweeper watches, a margin TWAP parent no longer needs its risk bucket to exist first, and spot balance rows now carry a durable version you can rank by instead of guessing from timestamps.

## Added

### `GET /api/v1/sweeper/chains` lists the chains and assets the sweeper watches

A new public endpoint returns one entry per chain the sweeper monitors, each carrying the assets it sweeps there:

```typescript theme={null}
const { chains } = await sdk.sweeper.getChains();

for (const chain of chains) {
  console.log(chain.chainId, chain.name, chain.hub);
  for (const asset of chain.assets) {
    console.log(asset.symbol, asset.tokenAddress, asset.decimals, asset.minSweepRaw);
  }
}
```

Each `SweeperChain` carries its EVM `chainId` (a number — the service refuses ids above `Number.MAX_SAFE_INTEGER`, so it is always exact), a short `name`, a `hub` flag marking the settlement chain that deposits on every other listed chain are bridged into, and its `assets`. Each `SweeperChainAsset` carries the `symbol`, the `tokenAddress` to send on that chain, its `decimals` (`0` when the token does not publish them), and `minSweepRaw` — the minimum sweepable balance in raw base units, as a decimal string, because raw amounts can exceed `Number.MAX_SAFE_INTEGER`. A balance strictly below the minimum stays on the deposit address until a later deposit brings the total up to it; a balance exactly at the minimum sweeps.

The endpoint is unauthenticated, so a deposit page can show the supported set before the user has a session. Registration is unchanged and still covers everything: the address `register()` returns is identical on every listed chain and is watched on all of them, so there is nothing chain-specific to pass. Only an operator changes the set, and how fast that lands depends on the chain's token source: a hub backed by the active-asset registry picks up a listing or delisting on the resolver's own refresh with no restart, while a chain configured with an explicit token list changes only when that configuration is rolled out. The gateway serves the last answer from a short cache either way, so refetch rather than caching the set as permanent.

Declared responses are `200`, `500`, and `503`, and the listing is all-or-nothing — rather than publish a partial set the resolver refuses the whole call, so the `503` covers a sweeper with no configuration, a configured chain disabled by its own configuration, a chain whose id is not yet verified against its RPC (an unreachable RPC included), a chain reporting an id outside the JSON-safe range, and a resolver that could not answer. All are safe to retry.

`SweeperChain`, `SweeperChainAsset`, and `GetSweeperChainsResponse` are exported from `@0xmonaco/types`; the gRPC equivalent is `SweeperService.GetChains`, which needs no auth interceptor.

### Spot balance rows carry a durable version

Spot balance state is now ranked by a producer-owned row version rather than by a timestamp or by arrival order. `version` appears on `GET /api/v1/accounts/balances` and `GET /api/v1/accounts/balances/{assetId}`, on the gRPC `AccountBalance`, on the subscribe-time `balances` snapshot, and on live `balance_update` events. The same state carries the same value on every one of those surfaces.

Only a positive version is rankable. Apply a higher version, treat an equal version as an idempotent duplicate, and ignore a lower one for that whole row. `0` is the legacy sentinel and means exactly what an absent field means — unranked — so normalize the two together and fall back to a full field comparison rather than ranking an unranked row as oldest. Versions are comparable only within the same `(user, application, token)`.

Margin collateral is deliberately not versioned. A collateral transfer that moves the spot wallet row emits a genuine versioned spot event for that leg, but the `balances` channel also carries margin-collateral notifications — a parent to risk-bucket allocation, a margin-routed deposit — that are not `user_balances` rows, have no margin-account ID in their wire identity, and may report a notification amount rather than authoritative whole-row state. Those frames carry no version; handle them fail-open and refresh margin account state rather than suppressing them with a spot watermark.

Within one batch create or batch replace, every item's balance frame now reports the batch-terminal amount per key, because one durable sequence covers the whole batch and equal-version frames are indistinguishable to a consumer applying them.

`@0xmonaco/react`'s `useUserBalances` does this ranking for you: equal or older whole-row deliveries are idempotent, and unversioned margin notifications are applied fail-open.

### `MESSAGE_RATE_LIMIT` on the WebSocket error frame

`WebSocketErrorCode` gains `MESSAGE_RATE_LIMIT`. Add a branch for it in `onError`; every existing code is unchanged.

## Changed

### Order rate limits are tighter in three ways

**`reduceOnly` no longer selects the risk-reduction class.** Reduce-only orders and reduce-only batch items draw the order-creation budget like any other new order. The flag is caller-controlled, so letting it select the 2× class would have let any client buy the larger budget by flagging its own traffic. The exits that must stay reachable when creation is exhausted are cancels — which keep the 2× budget — and position closes, which draw no order budget at all.

**Mass exit and TWAP creation are metered per call.** `POST /api/v1/orders/batch-cancel-all` and its by-pair form (gRPC `BatchCancelAll` / `BatchCancelAllByPair`) draw one risk-reduction item per call, however many orders they enumerate; `POST /api/v1/orders/twap` (gRPC `CreateTwapOrder`) draws one creation item per call. All three now declare `429` / `RESOURCE_EXHAUSTED` in the contract.

**Sub-accounts share a family budget.** Every account belongs to a family — a master account with all of its sub-accounts, or a standalone account alone. A gated request must fit both the account tier and the family tier, which holds the same caps times the family multiplier (currently 4×) and is charged by the master and every sub-account alike. An account with no sub-accounts can never hit it, since the family tier holds a multiple of its own cap. A family-tier rejection names the family in its message, and `x-ratelimit-remaining-second` / `-minute` now report the tighter of the two tiers.

Enforcement still rolls out per environment through the existing flag; until it is on, over-budget requests are admitted and only measured. See [Rate limits](/developers/rate-limits) for the full tables.

### Inbound WebSocket messages are capped per connection

Every application frame you send — `Authenticate`, `Subscribe`, `Unsubscribe`, `Ping`, or anything unparseable — draws from a per-connection budget, defaulting to 20 messages per second sustained with a burst of 300 (both operator-configurable). Protocol-level `Ping` frames count, since each costs the server a `Pong` write; protocol `Pong`, `Close`, and continuation frames do not. The first frame past the budget receives an `Error` frame with code `MESSAGE_RATE_LIMIT`, after which the connection closes with `1008` and the reason `message rate limit exceeded`. The budget is per connection and is unaffected by other connections from the same account.

A normal client never approaches it. Subscribe to everything you need in as few `Subscribe` frames as possible — one frame carries many channels — and let the roughly 25-second heartbeat idle. The TypeScript SDK sends every held channel in a single frame on open and on every reconnect, and the default burst is sized above the 256-channel subscription cap, so even adding a full subscription set one channel at a time on a live connection stays inside it. Treat `MESSAGE_RATE_LIMIT` in `onError` as a signal to slow down and batch, not to reconnect immediately. See [WebSocket reference](/reference/websockets#inbound-message-rate-close-code-1008).

### Perp maker trade legs report an empty spot balance snapshot

A margin fill leg never touches a spot `user_balances` row — its collateral settles through the risk engine — but the movement written for a margin maker's leg was walking the row anyway, so its balance snapshot fields described a movement that did not happen and could come out negative. Those legs now carry the empty snapshot the durable-log contract specifies for them, which movement history (`GET /api/v1/accounts/movements`) renders as `0` in `balanceBefore`, `balanceAfter`, `lockedBefore`, and `lockedAfter` (and their raw forms). A margin taker's own legs already carried the empty snapshot; the maker side is what was inconsistent. A genuine spot maker matched by a margin taker is unaffected either way: that side really does move a spot row, and it still reports its authoritative snapshot. This is a history-surface correction — a fill emits no live `movements` frame in either case, so nothing changes on the WebSocket. The response shape is unchanged; read margin account state for collateral, not a margin movement's balance fields.

## Fixed

### A margin TWAP parent provisions its risk bucket on first use

Creating a margin TWAP parent for a pair with no risk bucket yet returned `400` (gRPC `INVALID_ARGUMENT`), even though a plain margin order on the same pair provisions the bucket at admission. The two doors now accept the same first-use request: the create provisions the bucket before the parent reaches the engine, and the response's `riskBucketId` names it.

`marginMode: "CROSS"` with no active cross bucket for the margin account provisions the cross bucket without moving collateral — the slices fund it from the margin account as they reserve, exactly as later cross orders do. An accepted cross parent also joins its pair to the cross bucket's derived scope, as accepted cross orders do.

`marginMode: "ISOLATED"` (the default) with no bucket for `tradingPairId` — or a closed one, named explicitly or through `strategyKey` — seeds the deterministic isolated bucket from the margin account for the **first child only**: the estimated first slice's initial margin at the mark, plus the 10% market reserve band (not the parent's tighter slice band), plus a taker-priced fee buffer, rounded up to the collateral asset's precision. Every later slice tops the bucket up from the margin account's free collateral as it reserves, so the whole parent's margin is never earmarked ahead of need, and a slice the account can no longer cover is skipped rather than fatal. If the engine then refuses the parent, the seed is released back to the margin account.

Two edges are worth knowing. A schedule the engine could not slice — a window shorter than two slices at the style's cadence, or a quantity larger than the window's slice budget at the pair's maximum order size — is refused before anything moves. And if the engine's reply is lost after the seed moved (a transport failure rather than a rejection) the seed is kept, because the parent may exist, and the create returns the transport error. A fault inside the provisioning itself is sanitized: an internal fault returns `500` carrying `First-use risk bucket provisioning failed` (the underlying persistence or transport detail is logged, never returned) — the gRPC status message is exactly that sentence, while the REST envelope prefixes it as `Internal server error: First-use risk bucket provisioning failed`. A validation refusal from the provisioning rail returns `400` with that same sentence followed by its own detail.

A `reduceOnly` parent still requires an existing bucket — there is no position to reduce without one — and keeps its previous `400` remedies. See [TWAP orders](/trading-mechanics/twap-orders).

## Upgrade

```bash theme={null}
npm install @0xmonaco/core@1.0.56 @0xmonaco/types@1.0.56 @0xmonaco/react@1.0.56 @0xmonaco/contracts@1.0.56
# or bun
bun add @0xmonaco/core@1.0.56 @0xmonaco/types@1.0.56 @0xmonaco/react@1.0.56 @0xmonaco/contracts@1.0.56
```

Two things to check before upgrading. If you submit `reduceOnly` orders at rate, they now draw the creation budget rather than the 2× risk-reduction budget, so size your submission rate against creation and keep cancels as your exit path; if you run sub-accounts, account for the family budget on top of each account's own. And if you hold a WebSocket open, add a `MESSAGE_RATE_LIMIT` branch to `onError` and make sure your reconnect path does not re-subscribe channel by channel in a loop — the connection now closes with `1008` when the inbound budget is spent.

Everything else is additive. `version` on a spot balance row is optional and absent is not an error, so normalize absent and `0` together before comparing rather than treating `undefined` as the oldest revision. The margin TWAP fix removes a rejection rather than adding one, and `sdk.sweeper.getChains()` is a new call nothing else depends on.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.