> ## 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.59

# Monaco Protocol SDK v1.0.59

This release covers the consumer-facing changes since v1.0.58, and it is a wide one. **BuilderCodes** becomes a public API surface: an enrolled frontend can now read its own configuration, payout history, per-token totals and claimable balance, and move accrued revenue share into a tradeable balance with `sdk.buildercodes.claimBuildercodeRewards`. Because a frontend's payout wallet is often a multisig, wallet login gains a narrow **EIP-1271 contract-signature** path — offered only to a wallet holding a BuilderCodes payout bucket — which adds a retryable `503` to `POST /api/v1/auth/verify` and lengthens the challenge window to 30 minutes on any deployment where that path is enabled. Positions gain a durable `version` on the REST and gRPC position reads and both WebSocket position surfaces — the risk read is the one exception and carries none — and the `positions` channel now carries `riskBucketId`, `marginMode`, `maintenanceMarginRequired` and `initialMarginRequired`, so a handler no longer needs a REST read to learn a position's scope or margin requirement. The matching engine also starts bounding how many orders one account may keep resting — 500 per market and 4,000 per account by default, tunable per deployment — rejecting the excess with the structured code `ORDER_CAP_EXCEEDED`. If you keep more than a few hundred quotes resting per market, reconcile positions on a version, or authenticate a smart-contract payout wallet, read the upgrade notes before you bump.

## Breaking

### `BridgeRoute` gains `dstEid`, and `RouteKind` gains `Oft`

`@0xmonaco/contracts`: `SWEEPER_FACTORY_ABI`'s `BridgeRoute` tuple gains a `dstEid` field (`uint32`, between `compose` and `bridgeToken`). Encoding a `setRoute` call, or decoding a `route()` / `RouteSet` result, against the previous shape now mismatches. `dstEid` is the route's own LayerZero destination endpoint id: `0` uses the chain-wide hub EID, and a non-hub value builds a multi-hop lane — the funds land on the same clone address on the middle chain, whose own route relays them onward.

`RouteKind` gains `Oft` (`4`), a vanilla LayerZero OFT/OFTAdapter lane spoken with the standard `send` rather than Stargate's `sendToken`, used for direct WBTC bridging over BitGo's OFT mesh. `SWEEPER_ABI` gains the `OftCannotUnwrap` error: an `Oft` leg with `unwrapNative` is refused, because a plain OFT takes value only as fee.

Nothing is deployed on-chain with the previous ABI shape, so this only affects code that encodes or decodes those tuples itself.

## Added

### The BuilderCodes API

`sdk.buildercodes` is a new authenticated namespace with five methods, matched by REST endpoints and by the gRPC `monaco.api.buildercodes.BuildercodeRewardsService`.

| Method                                       | Endpoint                                   | Returns                                                                                               |
| -------------------------------------------- | ------------------------------------------ | ----------------------------------------------------------------------------------------------------- |
| `getConfig()`                                | `GET /api/v1/buildercodes/config`          | `applicationId`, `enrolled`, optional `payoutAddress`, `buildercodesEnabled`, `buildercodesBudgetBps` |
| `listPayouts({ page?, pageSize? })`          | `GET /api/v1/buildercodes/payouts`         | payout rows plus `total` / `page` / `pageSize` / `totalPages`                                         |
| `getPayoutSummary()`                         | `GET /api/v1/buildercodes/payouts/summary` | `totalPayouts`, nullable `lastCreditedAt`, and `byToken` totals                                       |
| `getRewardsBalance()`                        | `GET /api/v1/buildercodes/rewards/balance` | per-token `available` balances                                                                        |
| `claimBuildercodeRewards({ token, amount })` | `POST /api/v1/buildercodes/rewards/claim`  | `payoutBalance` and `tradingBalance` after the claim                                                  |

Every one of them is authenticated and caller-scoped: the payout bucket is resolved server-side from the caller's own wallet and never from the request, so a caller can only ever read or claim their own. The three application reads derive the application from the session and require its **current** payout wallet; `getRewardsBalance` derives the authenticated wallet's own reserved bucket, so a **former** payout wallet can still see and claim what it accrued before a rotation. Delegated-agent sessions are refused on all five. Payout rows expose `tradeId`, `payoutAddress`, `token`, a RAW-unit `amount` string, `appliedBps` and `createdAt`; internal row, application and user ids are not exposed.

The claim is ledger-only — no on-chain transaction — and lands in a trading balance, from where the ordinary withdrawal flow applies. `amount` is a positive integer in RAW atomic units and `token` a `0x`-prefixed EVM address; the SDK validates both before sending. A `403` means the caller is refused (BuilderCodes disabled, or a delegated session, which is permanently unsupported here rather than a rollout state), and a `409` means the payout balance is insufficient — including a wallet that has never accrued a share.

Treat an ambiguous failure on the claim as an **unknown outcome, not a rejection** — and that is not only `503`. The command is admitted to the sequencer before its reply is sent, so a claim that timed out in transit may still commit, and there is no idempotency key, so a blind retry can claim twice. The SDK enforces that by clearing `retryable` on **any** status at or above `500` and on a failure carrying no status at all (the response was never seen), against its usual "every 5xx is retryable" rule; a `4xx` is a decided outcome and keeps its classification. A balance read does not settle it either, because an admitted claim may still be queued. Two `503`s are the exception and are both tagged, because both are refused before admission and committed nothing: a maintenance block (`error: "OPERATIONS_BLOCKED"`) and a sequencer overload (`code: "OVERLOADED"`), which positively wants a retry with backoff.

See [BuilderCodes](/developers/buildercodes) for the revenue waterfall and the rotation rules, and [`sdk.buildercodes`](/sdk/typescript/buildercodes) for the method reference.

### Resting-order caps

The matching engine now bounds how many orders one account may keep resting on the book. The engine defaults are **500 per market** and **4,000 per account**, per engine shard, and both are set per deployment through `RESTING_ORDER_CAP_PER_MARKET` and `RESTING_ORDER_CAP_PER_ACCOUNT` — development already runs a per-account cap of 8,000 — so read the limit that applies to the environment you trade on rather than assuming these numbers. A new rest-capable order beyond either cap is rejected with `400` and the structured code `ORDER_CAP_EXCEEDED` (gRPC `INVALID_ARGUMENT` with the same code, per item in a batch), and the message names the binding scope.

This is a book-state guard, not a rate limit. Fills and cancels free slots immediately, so the cap never throttles a balanced quoting loop; `MARKET` and `IOC`/`FOK` orders are always exempt because they never rest; replaces are exempt, so **repricing at the cap always works**; and batch-cancel-all is exempt from these caps — though it keeps its own separate ceiling of 20,000 matching active orders, unchanged by this release (see [Other limits](/developers/rate-limits#other-limits)). Conditional (TP/SL) and TWAP orders keep their own dedicated caps and count against these only once triggered onto the book. See [Resting-order caps](/developers/rate-limits#resting-order-caps).

### A durable `version` on the position reads

Positions now carry an opaque, non-negative `version` — a producer-owned row revision — on the REST list and detail reads, the gRPC `Position` message, the WebSocket `positions` snapshot, and ranked live `position_update` frames. Reconcile on `positionId + version`, comparing only within the same position: higher is newer, and never order by `updatedAt`.

**The risk read is not a versioned surface.** `GetPositionRiskResponse` (REST `/positions/{positionId}/risk`, gRPC `GetPositionRisk`, `sdk.positions.getPositionRisk`) carries no `version` at all — neither a number nor a `0` — so reconcile risk against a position you ranked from a list, detail or WebSocket read rather than expecting the risk payload to rank itself.

The merge rule is **per field group**, not per frame. `markPrice`, `unrealizedPnl`, `liquidationPrice`, `leverage`, `maintenanceMarginRequired` and `initialMarginRequired` are unversioned — they are re-derived on every read rather than served from the versioned row — so on an *equal* version you skip the versioned fields as an idempotent redelivery but still take those six from the newer payload. Discarding a whole payload on an equal version freezes displayed PnL until the position's next real mutation. `isolatedMargin` is the one versioned field that is also exempt: the WebSocket positions snapshot overlays it from an independent live matching-engine read rather than from the versioned row, so it can reflect a later command than the version names — take it from the newer payload too. `0` is the unranked sentinel: treat it exactly as an absent version and fall back to a full field comparison. See [`version`](/sdk/typescript/positions) for the full rule and [the positions channel](/sdk/typescript/websockets#positions-channel) for which frames are ranked.

### Permissionless sweeps take a LayerZero fee

`@0xmonaco/contracts`: `SWEEPER_ABI`'s parameterless `sweep(address)` and `SWEEPER_FACTORY_ABI`'s permissionless `sweep(applicationId, user, token)` are now `payable`, so any caller can drive a direct plain-OFT or Stargate lane by fronting the LayerZero fee, with excess refunded to the caller; on the factory the value is forwarded to the clone, so a fresh deposit address deploys and sweeps in one call. `SWEEPER_ABI` gains `sweepWithRefund(token, feeRefundAddress)`, the parameterless sweep with an explicit refund address.

Direct Stargate routes join the permissionless path with a guard: the implied pool fee must fit the new owner-set `permissionlessBridgeFeeCapBps` (`setPermissionlessBridgeFeeCapBps`), whose default `0` admits only fee-free or rewarded sends, making the path opt-in per chain. New errors, and they are declared on different contracts — decode reverts against the right ABI: `BridgeFeeTooHigh` (pool fee above the cap) and `UnexpectedBridgeDebit` (the bridge debited something other than the pre-rounded amount) belong to `SWEEPER_ABI`, while only `InvalidFeeCap` (a cap above 100%) is on `SWEEPER_FACTORY_ABI`. The CCTP, hub and swap branches still refuse value, existing zero-value calls are unaffected, and the executor's quoted path is unchanged.

## Changed

### Contract-wallet login for BuilderCodes payout wallets

`sdk.auth.signChallenge` is no longer documented as signing with the wallet's private key. It asks the connected wallet to **authorize** the message: an EOA signs it with EIP-191 `personal_sign`, while a smart-contract wallet such as a Safe has no private key and returns a signature its own contract accepts, which the server checks under EIP-1271. The wallet client handles the difference and the returned value is submitted unchanged either way — so do not assume the 65-byte EOA shape, because a contract signature is usually longer.

This is **not** general smart-contract-wallet login. Server-side EIP-1271 verification is offered only to a wallet holding a BuilderCodes payout bucket, only while the deployment's contract-wallet flag is on, and only where it has a chain RPC endpoint configured; an ordinary smart-contract trading account still takes the recovery path and cannot authenticate. An eligible address that turns out to be an ordinary EOA is still verified by recovery — the two paths are mutually exclusive.

Three client-visible consequences:

* **`POST /api/v1/auth/verify` can now answer `503`** (gRPC `AuthService.Verify` → `UNAVAILABLE`). It covers three contract-verification cases — the chain did not answer, it did not answer within the deadline, or the server was at its concurrent-verification capacity and declined to ask, which is local saturation rather than a chain fault — and one that is not about contract wallets at all: verify reads private-beta feature-flag state before anything else, and a read that cannot be served surfaces as the same retryable `503` for any address, ahead of any signature or eligibility work. Do not diagnose every `503` here as a chain fault. It is retryable and is **not** a verdict on the signature: a rejected signature is still `401` / `UNAUTHENTICATED`, and so is a wallet contract that rejects by reverting. An eligible address can see the `503` even when it turns out to be an EOA, because the code lookup precedes mode selection. Addresses that are not eligible never reach the chain and are unaffected.
* **`signature` has a published length bound.** A signature over 8192 decoded bytes (16386 characters with the `0x` prefix) is rejected as an invalid signature rather than forwarded to the chain. The limit was already enforced; publishing it means a large valid signature no longer fails against an undocumented constraint. It is far above any realistic owner set — well over a hundred 65-byte slots — and EOA signatures are unaffected.
* **The challenge lifetime is 30 minutes** rather than 5 on any deployment where contract-signature login is enabled, because a multisig must collect owner approvals before it can answer. The window is uniform for **every** address, not eligibility-specific: `/auth/challenge` is unauthenticated and returns `expiresAt`, so a per-address lifetime would disclose which addresses hold a BuilderCodes payout bucket. A longer window is a longer-lived offer to authorize one predetermined keypair once, never a longer-lived credential.

### The `positions` channel carries scope and margin requirements

`sdk.ws.positions(...)` now surfaces `riskBucketId`, `marginMode`, `maintenanceMarginRequired` and `initialMarginRequired` on both the subscribe-time snapshot rows and the live `position_update` frames, mirroring the REST fields of the same names. The frame parser built its result from an explicit field list, so these were dropped even once the server sent them; a handler no longer needs a REST read to learn a position's risk bucket, margin mode, or current requirement at the mark.

All four are read leniently — absent stays `undefined` — rather than required. `riskBucketId` and `marginMode` are genuinely optional, since a legacy unbucketed position has neither, and treating the margin requirements as required would throw inside the WebSocket message handler and drop a risk-critical update whenever a frame arrived from an older deployment mid-rollout. **Do not infer either scope field from the other**: they resolve from different sources, so an id can arrive with no mode. Treat a missing `marginMode` as unknown and keep the mode you already hold — defaulting it to `CROSS` renders an isolated position with the wrong backing. The exported `MarginMode` union from `@0xmonaco/types` names the two values.

`updatedAt` is unchanged in shape but now reports when the position was last **mutated**, read from the producer's own record of the mutation. It previously stamped the serialization instant on live frames and on any REST read served from the live matching engine, so it reported the *read* rather than the write: no two reads of an unchanged position agreed, and a re-emitted frame was indistinguishable from a real change. Two frames for an unchanged position now repeat one timestamp, and a valuation-only oracle reprice no longer moves it. It remains display metadata — `version` is still the reconciliation key.

That match is exact for an **open** position served from a reachable matching engine. A terminal row, and any position read after a matching-engine restart, is served from the persistence clock instead and differs by the persistence lag. One case is not lag-bounded: while the matching engine is unreachable, a REST read of an open position falls back to that row's lifecycle timestamps and can report its opening instant until funding settles, while a live frame stays current.

`Position.marginMode` on a live-served REST or gRPC read is now answered by the matching engine's own view of the risk bucket rather than by a separate lookup, so a REST read and the `position_update` frame describing the same position cannot disagree. The value itself is unchanged — both derive from the same bucket.

### Rate limits: family tiers everywhere, and a budget on authentication

Account families now cover every metered class. The movement budget (withdrawal initiation, collateral transfers) and the per-account read budget each gain a family tier at 4× the account caps by default — the multiplier is set per deployment through `MOVEMENT_RATE_LIMIT_FAMILY_MULTIPLIER` and `READ_RATE_LIMIT_FAMILY_MULTIPLIER` — shared by a master account and all of its sub-accounts, so N sub-accounts no longer mint N full budgets; a family-tier rejection says so in its message.

`POST /api/v1/auth/challenge` and `/verify` (gRPC `AuthService/Challenge` and `Verify`) run before any account exists, so they are now metered per **client address**: 60 requests/min sustained and burst 120 by default, shared by both endpoints, on public ingress only. Like every other budget these are deployment defaults — `AUTH_RATE_LIMIT_PER_MINUTE` and `AUTH_RATE_LIMIT_BURST` override them — so pace against your environment's published figures rather than these. A login costs two. The check runs **before** the signature, so a throttled request is never an authentication failure — it is REST `429` carrying `Retry-After` and `details.retryAfter`, or gRPC `RESOURCE_EXHAUSTED` carrying a `google.rpc.RetryInfo` detail. The hint is transport-specific, so a gRPC caller reads `RetryInfo` rather than looking for a `retryAfter` field. Like the other limiters this ships warn-only: until `auth_rate_limit_enforced` is turned on for the environment, an over-budget request is admitted with a warn log and no `429` is returned at all. Every other authenticated call is unaffected; keep a session alive with `POST /api/v1/auth/refresh` rather than re-authenticating. See [Rate limits](/developers/rate-limits#authentication).

## Fixed

### `payoutAddress` may be `null`

`GetBuildercodesConfigResponseSchema` now accepts a `null` `payoutAddress`, where it previously allowed only a string or an absent key. This is wire-contract hardening rather than a fix for an observed response: the field is serialized without skip-serializing, so `null` is a representable shape, and the schema now matches it the way `lastCreditedAt` already did. A successful `getConfig()` still always carries an address in the released endpoint — it authorizes the caller as the application's payout wallet and returns `403` when none is configured — so the practical effect is that a client cannot throw on a nullable payload. The inferred type widens to `string | null | undefined`.

### `buildercodesBudgetBps` and `appliedBps` are different numbers

The two BuilderCodes bps fields are documented distinctly across the OpenAPI, TypeScript and Rust surfaces. `buildercodesBudgetBps` on `getConfig()` is the configured **target**, computed from the gross non-negative protocol take and capped by the post-floor remainder. `appliedBps` on a payout row is the **realized** whole-bps rate after that clamp, and may be lower. Field types and wire values are unchanged.

## Upgrade

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

Four things to check before upgrading. If you keep large numbers of orders resting, count them per market and per account: past your environment's caps (500 and 4,000 by default, but tunable per deployment) a new **rest-capable** `LIMIT` order is rejected with `ORDER_CAP_EXCEEDED` — `IOC` and `FOK` limit orders are exempt, since they never rest — so handle that code rather than the numbers, and prefer a replace over cancel-then-create, since replaces are exempt. If you reconcile positions, adopt the per-field-group merge rule — taking a whole payload only on a higher version freezes PnL at an unchanged one. If you authenticate a smart-contract payout wallet, treat `503` from `/auth/verify` as retryable rather than as a rejected signature, and stop assuming a 65-byte signature. And if you decode `BridgeRoute` yourself, re-encode against the new tuple.

The rest is additive. The four new `positions` fields are all optional, and `sdk.buildercodes` is a new namespace that no existing call touches. Among the version-bearing reads, `version` has two unranked representations rather than one: a REST or gRPC position read served from live matching-engine state reports `0`, because the engine holds the state but not the version, while an unranked live WebSocket frame and a legacy row omit the field entirely. Handle both — neither ranks, and neither is older than anything. The risk read is separate again: it has no `version` field to carry either form.
