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

# Monaco Protocol SDK v1.0.58

This release covers the consumer-facing changes since v1.0.57, and it is about one thing: self-trade prevention. An order that would fill against one of your own resting orders is now resolved under a **mode**. Previously the engine silently skipped your resting orders and filled around them, rejecting the whole order only when the remaining third-party depth could not cover it; now the mode decides both halves — and the platform default `CANCEL_MAKER` cancels your resting order in full and lets the incoming one carry on into the rest of the book — except where the incoming order is a zero-fill `MARKET` order with no other depth, which is rejected before sequencing and leaves your resting order untouched (below). The whole-order `SELF_TRADE_NOT_ALLOWED` rejection is removed from the match path, surviving only for a `FOK` order under `CANCEL_BOTH`. You choose the mode per order with the new `selfTradePreventionMode` field, or once for your wallet through new `GET` / `PUT /api/v1/accounts/self-trade-prevention` endpoints. "Your own" also widens: it now means your whole wallet family rather than a single account id, so a wallet trading against itself across two applications or two sub-accounts is caught where it previously was not. Cancellation events gain `stpCounterpartyOrderId` and the terminal reason `SELF_TRADE_PREVENTION` to name why an order went away. If you quote both sides on one wallet, parse order errors, or branch on a zero-fill order's status, read the three upgrade notes before you bump.

## Changed

### Self-trade prevention dispatches by mode

An incoming order that meets one of your own resting orders is resolved by the **taker's** effective mode; the modes on resting orders are never consulted. The mode resolves as the order's own `selfTradePreventionMode` if it carried one, else your wallet's stored default, else the platform default `CANCEL_MAKER`.

| Mode | Your resting order | Your incoming order |
| - | - | - |
| `CANCEL_MAKER` (platform default) | Cancelled in full, releasing its lock or reserve exactly as a user cancel does | Continues against the rest of the book and fills or rests per its own time in force |
| `CANCEL_TAKER` | Left resting, untouched — including any of your orders deeper in the path, since the walk stops | Remainder cancelled at the first self-touch; fills already made stand |
| `CANCEL_BOTH` | Every one of your resting orders in the marketable path is cancelled | Remainder cancelled |
| `SKIP` | Left resting; the taker trades around it | Continues against other liquidity |

One thing changed for every caller, whichever mode applies: the former whole-order `SELF_TRADE_NOT_ALLOWED` rejection — message `"Self-trade is not allowed"` — is gone from the match path, so a crossing order that used to be refused at its owner's own touch now resolves under a mode instead. What happens next is the mode's business, per the table above. On the default `CANCEL_MAKER` specifically, a crossing `GTC` order sweeps the owner's resting order and proceeds, where the same order previously rejected and the resting orders were silently skipped.

Post-only is the exception to all of it: the crossing check at admission is ownership-blind and runs **before** self-trade prevention, so a `postOnly` order that would cross is still rejected with `POST_ONLY_WOULD_CROSS` and cancels nothing of yours.

**One exception, and it is the default mode's:** the cancellation is decided during simulation and only commits if the order itself is sequenced. A zero-fill `MARKET` order whose only in-band depth was your own resting order is rejected for insufficient liquidity **before** the sequence is allocated — your own orders are deliberately not counted as available depth — so the simulated cancellation is discarded and **your resting order survives**. A rejected taker is therefore not evidence that your quote was removed.

`SKIP` will not rest you into your own book: a `GTC` remainder that would rest while one of your traded-around orders survives in the crossed range is cancelled instead of resting. A remainder that rests without crossing rests normally.

`SELF_TRADE_NOT_ALLOWED` survives in exactly one case — a `FOK` order under `CANCEL_BOTH` that meets one of your own orders, rejected at placement, and reported per item in batches. A `FOK` under `CANCEL_TAKER` or `SKIP` is instead cancelled whole with zero fills, and a `FOK` under `CANCEL_MAKER` cancels your makers first and then fills or kills as normal.

"Your own" is now the **wallet family**: every account sharing one wallet address, across every application. Previously the check compared a single account id, so one wallet trading against itself through two frontends or two sub-accounts was not detected.

### An order cancelled by self-trade prevention is `CANCELLED`, not `REJECTED`

A cancellation caused by self-trade prevention persists as a real `CANCELLED` order and emits `OrderCancelled`, **including when it filled nothing**. That is deliberately distinct from an accepted zero-fill `IOC` / `FOK` order, which is `REJECTED` for want of qualifying liquidity. For a `FOK` that is not the same as an empty book: partial candidate depth is discarded whenever it cannot fill the entire quantity, so a `FOK` can reject with liquidity plainly visible. A plain zero-fill `MARKET` submit is refused before acceptance and emits no order event at all. Status alone therefore distinguishes "my own prevention ended this order" from "there was not enough qualifying liquidity", with no message parsing. Code that treats a zero-fill order as necessarily `REJECTED` needs to handle `CANCELLED` too.

### Cancellation events name the counterparty

`OrderCancelled` gains an optional `stpCounterpartyOrderId` (wire `stp_counterparty_order_id`), and `OrderTerminalReason` gains `SELF_TRADE_PREVENTION`. The two always travel together, so the presence of the key is the whole signal — absent means this was not a self-trade-prevention cancellation. On a resting order swept by an arriving order of the same wallet family it names the **arriving** order; on a taker whose remainder was cancelled instead it names the **first own resting order** the walk reached. The `reason` field reads `self_trade_prevention`.

The field is declared on `OrderPartiallyCancelled` as well, but nothing emits it there: self-trade prevention cancels a resting order in full, so it never produces a partial cancellation. It is reserved for a future reducing mode and is absent on every frame today.

The cancellation is also persisted — the order row records the counterparty id — though no read endpoint exposes it yet, and rows written before the column read null.

### A swept resting **spot** order reports its lock release

When self-trade prevention cancels one of your resting **spot** orders, that order's wallet now receives the same movements-channel `user_movement` frame a user-initiated cancel produces: entry type `CREDIT`, transaction type `OrderCancelled`, referencing the cancelled order, with the atomic locked-before and locked-after snapshot. Previously the lock was released but only the order event was emitted, so a client tracking locked spot balances off the movements channel would have drifted out of sync with the exchange.

A swept **margin** maker emits no movement frame. Its reserve release moves no `user_balances` leg, so there is nothing for the movements channel to report — and a user-initiated cancel of a margin order emits none either, so the parity with an ordinary cancel holds in both modes. Do not wait on a movements frame for a margin order; track it on the `orders` channel.

## Added

### Per-order `selfTradePreventionMode`

`sdk.trading.placeLimitOrder`, `placeMarketOrder`, `replaceOrder`, `batchCreate` and `batchReplace` accept an optional `selfTradePreventionMode` of `"CANCEL_MAKER"`, `"CANCEL_TAKER"`, `"CANCEL_BOTH"` or `"SKIP"`, overriding your wallet and platform defaults for that order alone. The same field is on `POST /api/v1/orders`, `PUT /api/v1/orders/{orderId}` and both batch endpoints, and on the corresponding gRPC `OrdersService` requests.

```typescript theme={null}
await sdk.trading.placeLimitOrder(pair.id, "BUY", "0.10", "60000", {
  timeInForce: "GTC",
  selfTradePreventionMode: "SKIP",
});
```

A replacement is a new order with a new id, so it carries its own value and inherits nothing from the original — restate it to keep the override. The **requested** value is echoed back as `selfTradePreventionMode` on order detail and list reads and on order WebSocket payloads, and is absent when the request carried none; it is not the effective mode, so read the settings endpoint when you need that. In a batch, an item with an unknown value fails on its own with `error.code === "INVALID_SELF_TRADE_PREVENTION_MODE"` while the batch still returns `2xx` / `OK` — though the TypeScript SDK validates the field in its batch item schema and throws `ValidationError` first, so that per-item code is a raw REST/gRPC caller's shape.

The React `useTrade` hook accepts the field on the same calls, `batchReplace` included — its option types are derived from the core signatures, so no separate change was needed.

### Wallet self-trade-prevention default

`GET` and `PUT /api/v1/accounts/self-trade-prevention` read and set the authenticated wallet's default mode, as `sdk.profile.getSelfTradePreventionDefault()` and `setSelfTradePreventionDefault(mode)`, and as gRPC `AccountsService.GetSelfTradePreventionDefault` / `SetSelfTradePreventionDefault`.

```typescript theme={null}
const stp = await sdk.profile.setSelfTradePreventionDefault("SKIP");
console.log(stp.mode, stp.isCustom); // "SKIP" true
```

Both return `{ mode, isCustom }`, where `isCustom` is `false` when the platform default `CANCEL_MAKER` is being inherited and `true` once the wallet has chosen explicitly. The setting is **wallet-scoped**: one default per wallet address, shared by every application and account using it, and a per-order mode always overrides it. Mode tokens are always emitted in upper snake case. Raw REST and gRPC callers may send them in any case; the TypeScript SDK validates against the exact uppercase tokens and throws before sending, so pass those verbatim.

The write is sequenced through the matching engine and acknowledged only after the durable-log append, so the wallet's **next order already resolves the new mode**. The read serves an asynchronous projection of that same log, so a read issued immediately after a set can briefly lag the setter's own response. An unknown mode is `400` / `InvalidArgument` — the TypeScript SDK rejects it before sending. A delegated-agent session can neither read nor set the default: **both** operations return `403` / `PermissionDenied`. The setter also returns `503` / `Unavailable` when the matching engine is unreachable or the sequenced write fails transiently; treat that outcome as **unknown** rather than as proof nothing was written, since `DeadlineExceeded` and `Cancelled` map to it too and the write may have committed before the response was lost. Re-read, or retry the same `mode` — the call sets an absolute value, so a replay cannot land a different one.

The MCP tools `place_limit_order` and `replace_order` accept `selfTradePreventionMode` as well.

## Upgrade

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

Three things to check before upgrading. If you quote both sides on one wallet, the default `CANCEL_MAKER` now cancels your own resting quote in full where the old engine rejected the incoming order — set your wallet default to `SKIP` if you want the closest thing to the previous trade-around behaviour, and handle the `OrderCancelled` that a swept quote now produces. If you match on the message `"Self-trade is not allowed"` to detect a self-cross, that string no longer appears on the match path; branch on `terminalReason === "SELF_TRADE_PREVENTION"` or on the presence of `stpCounterpartyOrderId` instead. And if you assume a zero-fill order is `REJECTED`, handle `CANCELLED` as well.

The rest is additive. `selfTradePreventionMode` is optional everywhere it appears and absent on reads when the order carried none, `stpCounterpartyOrderId` is absent on every event that is not a self-trade-prevention cancellation, and the wallet default needs no action — every wallet inherits `CANCEL_MAKER` until it sets one. See [Self-Trade Prevention](/developers/trade#self-trade-prevention).


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