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

# Monaco Protocol SDK v1.0.60

This release covers the consumer-facing changes since v1.0.59. A take-profit or stop-loss and the order it fires into are now **linked in both directions**: a `TRIGGERED` conditional order carries `triggeredOrder` — what its close actually did — and the close carries `conditionalOrderId` back to the trigger, so an exit in order history can be attributed without scanning the conditional list. The same work changes how a `TRIGGERED` `CONDITIONAL` row renders in `getPaginatedOrders`: it now mirrors its close instead of reporting a synthesized `FILLED` with `filledQuantity: "0"`, which changes what a page of order history sums to. The cross risk-bucket preview **drops `selectedTradingPairIds`** — the field the server never priced anything from — and the SDK now rejects it rather than sending it. A new authenticated `sdk.managedMarkets` namespace gives an application's configured maker wallet its assignment inventory, a fenced lease and a bounded quote refresh, and public market discovery now hides an operator-managed launch until it is live. If you build cross previews, sum fees or quantities over an order page, or blind-replace resting orders, read the upgrade notes before you bump.

## Breaking

### A cross risk-bucket preview no longer takes `selectedTradingPairIds`

`sdk.marginAccounts.simulateRiskBucketOrderRisk` (`POST /api/v1/margin/risk-buckets/simulate-order-risk`, gRPC `MarginAccountsService.SimulateRiskBucketOrderRisk`) no longer accepts the field for `marginMode: "CROSS"`. The cross request is now `{ tradingPairId, marginMode: "CROSS", side, orderType, quantity, leverage?, price?, positionSide?, reduceOnly? }` — the isolated form minus `strategyKey`. Passing `selectedTradingPairIds` throws `ValidationError` naming `request.selectedTradingPairIds` before any request is made, and the wire body no longer carries it. `SimulateCrossRiskBucketOrderRiskRequest` types the field `?: never`, exactly as `strategyKey` already was, and `TransferCollateralToCrossRiskBucketRequest` made the same change to the same field one release earlier.

Cross scope is **derived from trading**: a pair joins the cross risk bucket when a cross order on it succeeds and, once its positions close, stays listed until a later successful cross order on another pair reconciles the bucket. The preview never priced anything from the caller's list — the engine prices the positions it holds whatever the request declared — so the three rules the field carried (non-empty, containing `tradingPairId`, covering every pair with an open cross position) only ever produced `400`s, which is why callers had taken to passing every margin pair.

The behaviour the server now allows is the inverse: a cross preview with **no list**, one whose list **omitted `tradingPairId`**, and one whose list **omitted a pair with an open cross position** all used to be rejected and now succeed, with unchanged preview figures. Raw REST and gRPC callers keep working — the wire field survives, marked deprecated, accepted and ignored, and sending it logs a server-side deprecation warning and has no other effect. It is still rejected for `ISOLATED`, as it always was, and that message now reads `selectedTradingPairIds is not supported for ISOLATED risk buckets`. The generated Rust REST request struct keeps the field with a deprecation note in its doc comment; the gRPC struct carries `#[deprecated]`, so build it with `..Default::default()` rather than naming the field, which trips rustc's `deprecated` lint under `-D warnings`.

The **response** field of the same name is unchanged in type (`?: string[]`) and changed in meaning: from `simulateRiskBucketOrderRisk` it now reports the **derived** scope the preview ran against — the active cross bucket's recorded pairs, every pair with an open position in it, and the previewed pair (the previewed pair alone before the first cross order) — never a caller-declared list. `simulateParentMarginOrderRisk` and `simulateOrderRisk` are unaffected; they never took the list.

The MCP `simulate_risk_bucket_order_risk` tool drops the field and its three refinements, declares `positionSide` the way the order tools do, and is now **strict**: `selectedTradingPairIds` — or any other unknown key — is refused with the key named rather than silently stripped. `strategyKey` stays `ISOLATED`-only.

## Added

### A take-profit / stop-loss and its close are linked in both directions

A conditional order fires on the **mark price** and then trades against the **book**, so the price it triggered at and the price its close filled at are independent numbers — nothing bounds one by the other, and they can coincide. Both sides are now readable from either end. Every read below needs an authenticated session and is owner-scoped: a caller only ever sees their own conditional orders and the closes they fired.

`ConditionalOrder` gains `triggeredOrder?: TriggeredOrderSummary`, a newly exported interface — `orderId`, `status`, `filledQuantity`, and optional `averageFillPrice`, `totalTakerFees`, `filledAt` and `terminalReason` — read from the triggered order's own row at request time. It is returned by `getConditionalOrder` (`GET /api/v1/orders/conditional/{conditionalOrderId}`), `listConditionalOrders` (`GET /api/v1/orders/conditional`) and under `Order.conditional` on `getPaginatedOrders` rows. It is present only once `state` is `TRIGGERED` and the close can be read: absent while waiting on the trigger, absent on a `FAILED` conditional, and **absent on the `conditional_orders` WebSocket frames**, snapshot and live alike. A `FAILED` conditional carries no summary because it never recorded a `triggeredOrderId` to join on. Whether a close row exists at all depends on **where** it failed: one the engine placed and the book then rejected persists as an order row carrying `conditionalOrderId`, while one refused before placement — a validation failure such as a bad quantity step — produces no order at all, and the conditional's own `failureReason` is the only record of it.

`status` is copied verbatim from the close's own row, so treat it as the full `OrderStatus` union and switch with a `default` branch rather than against a fixed list. What you will actually see: `SETTLED` once a fully filled leg has settled, and `FILLED` in the window before that — a filled order is a real persisted state on its way to `SETTLED`, not a value this field skips; `SUBMITTED` / `PARTIALLY_FILLED` for a `LIMIT` leg still resting; `CANCELLED` when the remainder was cut, which includes a `MARKET` or `IOC` leg that filled part of its quantity, so a `CANCELLED` close can carry a **nonzero** `filledQuantity` (read `terminalReason`); and `EXPIRED` for a `LIMIT` leg that lapsed.

`Order.conditionalOrderId?: string` is the reverse pointer: on an order the engine **placed** for a TP/SL trigger — including one the book then rejected — the conditional that fired it. A trigger refused before placement has no order row for this to sit on. It is served by the read APIs — `getOrder` and `getPaginatedOrders`, and their gRPC equivalents `GetOrder` / `ListOrders` — but **never on a WebSocket frame**: no order event and no snapshot row carries it, so a triggered close first seen through the socket acquires the field on the next read rather than never. It is absent on every order you placed yourself, on TWAP slice children (those carry `parentOrderId`), and on triggered closes written before the field existed: there is no backfill, so absence on an older close means unknown, not placed by hand. `@0xmonaco/react`'s `useUserOrders` carries it through as a REST-only field alongside `terminalReason`, `expirationDate`, `positionSide` and the fee aggregates.

Compare `triggeredOrder.averageFillPrice` with the conditional's `triggerPrice` to see what the close actually paid. The two are independent numbers rather than one bounded by the other: the trigger fires when the **mark** crosses the level, the mark is a reference price rather than the book's midpoint, and the worker samples it on a tick — so the mark can already be well past the level when the close is placed. A market close then pays the spread it crosses plus the depth its size consumes, which usually leaves the fill worse than the trigger, but nothing guarantees a minimum gap and a fast move can leave it better. Values for the **same state** are numerically equal to what `getOrder` returns, but the two reads are not guaranteed to see the same state: this summary is a repository read of the order row, while `getOrder` is cache-first and the read cache is fed by the live engine stream, so it can be ahead of the database by the persistor's lag. A `filledQuantity`, `averageFillPrice` or fee total that disagrees between the two is usually that lag, not a bug — read `getOrder` when you need the close's very latest state. Even for the same state the printed scale can differ, so never compare the raw strings with `===`: compare as exact decimals, normalizing the scale or using a decimal library, rather than through `Number()`, which is inexact.

`ConditionalOrder`, `TriggeredOrderSummary` and `ListConditionalOrdersResponse` are now registered in the wire-coverage tripwire, so a future field on the conditional read model fails `tsc` until the hand-written type carries it.

See [Trigger Price Is Not Fill Price](/developers/trade#trigger-price-is-not-fill-price).

### Managed maker assignments

`sdk.managedMarkets` is a new authenticated namespace with three methods, matched by REST endpoints and by the gRPC `monaco.api.managed_markets.ManagedMarketsService`. It drives the maker side of an operator-managed market launch: the wallet the operator configured for that application claims a fenced lease and refreshes a **server-bounded** quote set, rather than placing orders itself.

| Method                                                            | Endpoint                                         | Returns                                                     |
| ----------------------------------------------------------------- | ------------------------------------------------ | ----------------------------------------------------------- |
| `listAssignments({ page?, pageSize? })`                           | `GET /api/v1/managed-markets/assignments`        | `items`, `total`, `page`, `pageSize`, `inventoryObservedAt` |
| `claimLease(launchId, { ownerId, expectedGeneration? })`          | `POST /api/v1/managed-markets/{launchId}/lease`  | `ManagedMarketLease`                                        |
| `refreshQuotes(launchId, { ownerId, generation, quoteRevision })` | `POST /api/v1/managed-markets/{launchId}/quotes` | `ManagedMarketQuoteStatus`                                  |

All three need a **wallet** session — delegated-agent sessions are refused with `403`, permanently rather than as a rollout state — but the maker requirement is narrower than that: `claimLease` and `refreshQuotes` are launch-scoped and need a session matching both the configured maker user and the application, while `listAssignments` takes any authenticated wallet and filters by it, so a non-maker gets an empty page rather than an error. `ownerId` is a process fence, not authorization — it decides which of your own processes holds the lease, and grants nothing.

`generation` and `quoteRevision` are `uint64` fences carried as **exact decimal strings** in JSON and as `uint64` in protobuf. Never round-trip them through `Number`. On an unknown quote outcome, reconcile through `claimLease` before advancing the revision rather than retrying the quote.

Quote intents are server-derived end to end: a fixed preallocated risk bucket, server-chosen prices and fees, and no caller price, quantity or identity override. They never fund accounts.

See [`sdk.managedMarkets`](/sdk/typescript/managed-markets) for the method reference and the error map.

## Changed

### A `TRIGGERED` conditional row mirrors its close

In `getPaginatedOrders` (`GET /api/v1/orders`, gRPC `ListOrders`), a `CONDITIONAL` row whose conditional has **triggered** now reports the order it fired into instead of a synthesized `FILLED` with `filledQuantity: "0"`. `quantity` is the size the leg resolved to; `filledQuantity`, `averageFillPrice`, `quoteVolume`, `filledAt`, `cancelledAt` and `expiredAt` are the close's own; and `status` is the close's **current** status. The `status` filter applies the same rule, so `status=SETTLED` returns exactly the triggered rows whose close has filled and settled, and `status=SUBMITTED` includes triggered `LIMIT` legs still resting. A triggered row whose close cannot be read keeps the previous rendering.

The **fee fields stay absent on the `CONDITIONAL` row** — the close's own row carries them, and `conditional.triggeredOrder.totalTakerFees` repeats the total — so summing fees over a page counts each exit once. Quantities are the opposite case: the close is also listed as its own `MARKET` or `LIMIT` row, so sum `quantity` or `quoteVolume` over those rows only, or the triggered pair double-counts.

### A triggered leg can be cancelled but not replaced

`sdk.trading.replaceOrder` (`PUT /api/v1/orders/{orderId}`) and `sdk.trading.batchReplace` (`POST /api/v1/orders/batch-replace`; gRPC `ReplaceOrder` / `BatchReplaceOrders`) now refuse to replace the `LIMIT` leg a take-profit or stop-loss fired into — an order carrying `conditionalOrderId` — the way a TWAP slice child is refused. A replacement rests under a new order id the conditional order does not point at, which would sever the link in both directions and leave every read of the TP/SL reporting the exit as cancelled while the live replacement rested unattributed. The refusal is an `INVALID_ORDER` rejection naming the conditional order, and the original leg stays resting. Cancel the leg and place a new order instead; `cancelOrder` on a triggered leg is unchanged.

### Public market data hides an operator-managed launch until it is live

`GET /api/v1/market/pairs`, the pair detail reads, `GET /api/v1/market/screener` and the public price reads exclude an operator-managed market until its durable runtime projection reaches **live** — including when `isActive=false` is passed explicitly, so a pending, warming or halted managed launch cannot be surfaced by asking for inactive markets. A pair with no launch row is a legacy instrument and keeps its existing visibility, so nothing that trades today changes.

## Upgrade

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

Three things to check before upgrading. If you build **cross previews**, delete `selectedTradingPairIds` from the request — it now throws `ValidationError` client-side instead of reaching the server, and the response field of the same name means the derived scope rather than an echo of what you sent. If you **aggregate a page of order history**, re-read the `TRIGGERED` `CONDITIONAL` rule above: fees are safe to sum over the whole page, quantities are not. And if you **replace resting orders without inspecting them**, handle the `INVALID_ORDER` refusal on a leg carrying `conditionalOrderId` — cancel-and-replace is the remedy there.

The rest is additive, and the two new fields have **different histories**. `triggeredOrder` is a join performed at request time on the conditional's existing `triggeredOrderId`, which predates this release, so a `TRIGGERED` conditional from before the upgrade gains the summary too — the join deliberately accepts a close whose own reverse pointer is absent. `conditionalOrderId` is the one with no history: it is stamped going forward and never backfilled, so its absence on an older close means unknown rather than "not placed by a trigger". Neither is carried on a WebSocket frame — read both from REST. `sdk.managedMarkets` is a new namespace no existing call touches. `403` is reserved for delegated-agent sessions; a wallet that is simply not the application's configured maker sees an **empty** `listAssignments` page, and the launch-scoped calls answer `404` rather than confirming a launch it has no part in.
