# Accounts
Source: https://docs.0xmonaco.com/accounts
Understand where your funds sit and what each account is for.
Connect your existing wallet or use one created through Privy.
Your trading balance for individual spot markets.
Your collateral account for futures and other derivatives.
{subscribed ? "Live" : "Disconnected"}
{balances.map((balance) => (| Class | Public | PrivateLink | ||
|---|---|---|---|---|
| Per second | Per minute | Per second | Per minute | |
| Order creation | 500 | 15,000 | 2,000 | 60,000 |
| Risk reduction | 1,000 | 30,000 | 4,000 | 120,000 |
console.log(display, isCustom);
```
```bash theme={null}
curl "https://staging.apimonaco.xyz/api/v1/pitpass/codes/me" \
-H "X-Monaco-PublicKey: $MONACO_SESSION_PUBLIC_KEY" \
-H "X-Monaco-Timestamp: $MONACO_TIMESTAMP" \
-H "X-Monaco-Signature: $MONACO_SIGNATURE"
```
Response:
```json theme={null}
{
"code": "0xabc123...",
"createdAt": "2026-07-01T00:00:00Z",
"customCode": "",
"isCustom": false,
"display": "Monaco - 0xabc123..."
}
```
When the wallet has claimed a custom handle, `customCode` holds it (as entered), `isCustom` is `true`, and `display` is the handle.
### 3. Claim a custom handle (optional)
Let a user replace the wallet-derived default with a vanity handle. Preview availability as they type with the public check, then claim it. The handle is stored exactly as entered, must be letter-first `[A-Za-z0-9_]` and 5–20 characters, and is unique case-insensitively.
```typescript theme={null}
// Live availability feedback while the user types (public, no auth).
const { available, reason } = await sdk.pitpass.checkTraderCodeAvailability("acePilot");
if (!available) {
// reason ∈ "taken" | "too_short" | "too_long" | "reserved" | "invalid_char" | "looks_like_address"
console.log(`Unavailable: ${reason}`);
}
// Claim or edit (authenticated). The server is the authoritative gate.
const { display, isCustom } = await sdk.pitpass.setTraderCode("acePilot");
console.log(display, isCustom); // "acePilot", true
// Revert to the wallet-derived default.
await sdk.pitpass.clearTraderCode();
```
```bash theme={null}
curl "https://staging.apimonaco.xyz/api/v1/pitpass/codes/available?code=acePilot"
curl -X PUT "https://staging.apimonaco.xyz/api/v1/pitpass/codes/me/custom" \
-H "Content-Type: application/json" \
-H "X-Monaco-PublicKey: $MONACO_SESSION_PUBLIC_KEY" \
-H "X-Monaco-Timestamp: $MONACO_TIMESTAMP" \
-H "X-Monaco-Signature: $MONACO_SIGNATURE" \
-d '{ "code": "acePilot" }'
```
`setTraderCode` maps rejections to `409` (handle already taken, or a recently-freed handle still in cooldown), `422` (validation, with a reason code), `429` (edited too frequently), and `403` (custom handles are disabled). `clearTraderCode` counts against the same rolling-hour edit limit, so it can also return `429` or `403`. The public availability check is rate-limited and returns `429` once a caller exceeds the allowance — treat it as advisory feedback, not the authoritative gate.
### 4. Check rewards balance
Read accumulated rewards across all reward tokens. Returns an empty list if no rewards have been earned yet.
```typescript theme={null}
const { balances } = await sdk.pitpass.getRewardsBalance();
for (const entry of balances) {
console.log(`${entry.token}: ${entry.available} raw units`);
}
```
```bash theme={null}
curl "https://staging.apimonaco.xyz/api/v1/pitpass/rewards/balance" \
-H "X-Monaco-PublicKey: $MONACO_SESSION_PUBLIC_KEY" \
-H "X-Monaco-Timestamp: $MONACO_TIMESTAMP" \
-H "X-Monaco-Signature: $MONACO_SIGNATURE"
```
Response:
```json theme={null}
{
"balances": [
{
"token": "0xusdc...",
"available": "1200000"
}
]
}
```
`available` is in raw atomic units. Divide by the token's decimals (e.g., `10^6` for USDC) to display a human-readable amount.
### 5. Transfer rewards to a trading balance
Move earned rewards from the rewards bucket into the authenticated application's trading balance. The transfer is instant and ledger-only — no on-chain transaction. Each call spends the per-user [movement budget](/developers/rate-limits) that withdrawals and collateral transfers share, whatever the rewards balance; past it the answer is `429` with `Retry-After` once that budget is enforced.
```typescript theme={null}
const result = await sdk.pitpass.transferRewards({
token: "0xusdc...",
amount: "1200000", // raw atomic units, must be a positive integer string
});
console.log(result.rewardsBalance); // remaining in rewards bucket after transfer
console.log(result.tradingBalance); // new trading balance for this token
```
```bash theme={null}
curl -X POST "https://staging.apimonaco.xyz/api/v1/pitpass/rewards/transfer" \
-H "Content-Type: application/json" \
-H "X-Monaco-PublicKey: $MONACO_SESSION_PUBLIC_KEY" \
-H "X-Monaco-Timestamp: $MONACO_TIMESTAMP" \
-H "X-Monaco-Signature: $MONACO_SIGNATURE" \
-d '{
"token": "0xusdc...",
"amount": "1200000"
}'
```
Returns `409` when the requested amount exceeds the available rewards balance.
## Reward Rates
Monaco configures how many referral levels TraderCodes rewards — from 1 up to a hard maximum of 10 — and a separate reward rate for each level. Levels and rates can be configured per client application, and a trade pays the levels and rates of the client it was placed through. Reward amounts are a share of the protocol fee generated by the referred user's trades:
| Level | Relationship |
| - | - |
| L1 | Your direct referral's trades |
| L2 | Trades of the users your L1 referrals referred |
| L3 – L10 | Each further step down the chain, up to the configured depth |
* Each level's rate applies to the **TraderCodes base** of the fill: the part of the protocol fee Monaco sets aside for referral rewards.
* Every level takes its rate of that same base; levels do not compound on each other. The rates of all levels together never exceed 100% of the base.
* Rewards are computed at fill time and credited instantly to the referrer's rewards bucket. Each reward row records the `rateApplied` at that fill.
* Levels the chain does not reach — where it is shorter than the configured depth — are simply not paid.
* Configuration changes apply to future fills only. Rewards already earned at a level deeper than the current depth stay readable in your reward history, `earningsBySource`, and `summary.byLevel`.
## Builder Checklist
* Capture `?ref=CODE` from the URL before the user completes sign-in, and pass it as `sdk.auth.authenticate(clientId, code)` or via the raw auth verify endpoint — `sdk.login` does not forward a referral code.
* Validate the code with `getTraderCodeInfo` before showing a confirmation to the user; return a clean message on `404` instead of an error.
* Never block sign-in on referral-code validation failures — a bad code is silently ignored server-side, but front-end validation errors should not prevent login.
* Render the `display` field from `getMyTraderCode` rather than composing the display string yourself, and read `isCustom` to reflect whether a vanity handle is set.
* In a custom-handle claim form, debounce `checkTraderCodeAvailability` for live feedback, but always handle `409`/`422`/`429` on `setTraderCode` — the server is the authoritative gate, not the advisory check.
* Display `available` reward balances in human-readable form by dividing by the token's decimal precision.
* Handle `409` on `transferRewards` by refreshing the balance and showing the user their current available amount.
## Related References
* [Earn](/earn) for the product overview of TraderCodes and BuilderCodes
* [Auth](/developers/accounts/wallets) for the full sign-in flow
* [TypeScript TraderCodes SDK](/sdk/typescript/pitpass) for complete method signatures
* [REST — Get your TraderCode](/api-reference/pitpass/get-my-trader-code) for the generated schema
* [REST — Look up a TraderCode](/api-reference/pitpass/get-trader-code-info) for the generated schema
* [REST — Get rewards balance](/api-reference/pitpass/get-rewards-balance) for the generated schema
* [REST — Transfer rewards](/api-reference/pitpass/transfer-rewards) for the generated schema
# Vault Deposits & Withdrawals
Source: https://docs.0xmonaco.com/developers/vault
Approve tokens, deposit wallet assets into Monaco, and withdraw available balances back on-chain.
The Monaco vault is the bridge between a user's Sei wallet and Monaco account balances. Deposits move assets into Monaco for trading. Withdrawals move available balances back on-chain.
Trading inside Monaco is gasless, but deposits and withdrawals are on-chain wallet transactions. Users still need SEI gas for those settlement actions.
## Surface Map
| Surface | Use |
| - | - |
| REST | `POST /api/v1/withdrawals`, `GET /api/v1/withdrawals/{withdrawalIndex}`, `POST /api/v1/sweeper/register`, `GET /api/v1/sweeper/chains` |
| TypeScript SDK | `sdk.vault.approve()`, `sdk.vault.needsApproval()`, `sdk.vault.deposit()`, `sdk.vault.withdraw()`, `sdk.vault.retryWithdrawal()`, `sdk.vault.getAllowance()`, `sdk.sweeper.register()`, `sdk.sweeper.getChains()` |
| React SDK | `useVault()` |
| WebSocket | `balances`, `movements` for resulting account updates |
| gRPC | `WithdrawalsService`, `SweeperService` |
## Mental Model
Wallet assets are not tradable until they enter Monaco. Deposits move assets from the connected wallet into Monaco's vault. Spot orders reserve balances from the user's Monaco spot account. Perps trades use collateral held in the user's perps account.
Withdrawals move available balances back out of Monaco. Funds locked by open orders, pending operations, or required as perps collateral cannot be withdrawn until they are released.
## Deposit Flow
Create a Monaco session for the connected wallet before calling vault methods.
Monaco vault methods use asset UUIDs. The easiest way to find an asset ID is from a trading pair's `baseAssetId` or `quoteAssetId`.
Check whether approval is needed, then approve Monaco's vault contract to spend the ERC20 token if required.
Deposit to the spot account by default, or route directly to margin collateral when the integration is funding perps.
```typescript theme={null}
const pair = await sdk.market.getTradingPairBySymbol("BTC/USDC");
const assetId = pair.quoteAssetId;
const amount = parseUnits("100", 6);
if (await sdk.vault.needsApproval(assetId, amount)) {
await sdk.vault.approve(assetId, amount);
}
await sdk.vault.deposit(assetId, amount);
```
Allowances are wallet-side ERC20 approvals for Monaco's vault contract. They are not Monaco account balances, but they determine whether a deposit needs an approval transaction before the deposit transaction.
```typescript theme={null}
const allowance = await sdk.vault.getAllowance(assetId);
const needsApproval = await sdk.vault.needsApproval(assetId, amount);
console.log({ allowance: allowance.toString(), needsApproval });
```
## Deposit Directly to Perps Collateral
By default, `vault.deposit()` credits the user's spot account. Pass `"margin"` as the target when you want to route the on-chain deposit directly into perps collateral.
```typescript theme={null}
const pair = await sdk.market.getTradingPairBySymbol("BTC/USDC");
const assetId = pair.quoteAssetId;
const amount = parseUnits("500", 6);
await sdk.vault.approve(assetId, amount);
await sdk.vault.deposit(assetId, amount, true, "margin");
```
If margin routing is unavailable for the asset or account, Monaco falls back to spot. Keep this visible in your UI by refreshing both [account balances](/developers/account-balances) and [perps collateral](/developers/perps-collateral) after the transaction.
## Registered Deposit Addresses
Instead of prompting a wallet through the approve → deposit flow, you can give a user a stable, deterministic deposit address and have deposits of the watched assets swept into Monaco automatically — the transfer indexer filters to the token addresses `getChains()` lists, so an unlisted ERC20 sent there is not swept. Every `(application, user, deposit target)` triple maps to one such address; `sdk.sweeper.register({ depositTarget? })` (`POST /api/v1/sweeper/register`; gRPC `SweeperService.Register`) registers it so the backend monitors the address and sweeps a watched asset once its accumulated balance reaches that asset's minimum, crediting the user — no per-deposit signature or transaction from your app. A deposit that leaves the balance below the minimum stays on the address until a later one brings it up. The call is authenticated and carries no identity of its own: the application and the credited address both come from the session, so each user registers through their own client. It is idempotent and returns the derived address immediately, so you can display and fund it before any contract exists there. `depositTarget: "margin"` asks for swept deposits to land in the parent margin account's collateral; because the target is part of the derivation, spot and margin have two different deposit addresses. As with a direct deposit, margin routing that is unavailable for the asset or account falls back to spot, so refresh both balances rather than treating a margin address as proof of collateral credit. One registration covers every chain the sweeper watches: the derived address is identical on all of them and is monitored on all of them, so the request carries no chain. `sdk.sweeper.getChains()` (`GET /api/v1/sweeper/chains`; gRPC `SweeperService.GetChains`) lists that set — per chain its EVM chain id, name, whether it is the settlement hub deposits are bridged into, and the swept assets with their token addresses, decimals, and minimum sweep amounts in raw base units. It is public, so a deposit page can show where a user may send before they have a session; a balance below an asset's minimum waits on the address until a later deposit brings the total up to it (the sweep skips a balance strictly below the minimum, so an exact-minimum total sweeps). See the [Vault SDK guide](/sdk/typescript/vault#registered-deposit-addresses) for the full request/response shape and error cases.
## Withdrawal Flow
The user must be signed in before requesting a vault withdrawal.
Use a trading pair or an existing balance row to find the Monaco asset UUID.
Compare the requested amount against `availableBalanceRaw`, not total balance. Locked funds are not withdrawable.
Submit the withdrawal and wait for the on-chain transaction result.
```typescript theme={null}
const pair = await sdk.market.getTradingPairBySymbol("BTC/USDC");
const assetId = pair.quoteAssetId;
const balance = await sdk.profile.getUserBalanceByAssetId(assetId);
const amount = parseUnits("25", 6);
if (BigInt(balance.availableBalanceRaw) < amount) {
throw new Error("Insufficient available Monaco balance");
}
await sdk.vault.withdraw(assetId, amount);
```
## Asset IDs
Monaco uses asset UUIDs for vault operations instead of symbols or token addresses. This prevents ambiguity when assets share symbols, appear in multiple products, or map to different contracts across networks.
Trading pairs expose the asset IDs most builders need:
```typescript theme={null}
const pair = await sdk.market.getTradingPairBySymbol("BTC/USDC");
console.log({
baseAssetId: pair.baseAssetId,
quoteAssetId: pair.quoteAssetId,
baseContract: pair.baseTokenContract,
quoteContract: pair.quoteTokenContract,
});
```
Balance rows also include `assetId` for assets the user already holds.
## Builder Checklist
* Explain that deposits and withdrawals require wallet confirmations and SEI gas.
* Use asset IDs for vault operations; do not pass token symbols to deposit or withdrawal methods.
* Refresh balances after approval, deposit, withdrawal, or retry.
* Disable withdrawals for balances locked by open orders, pending operations, or perps margin requirements.
## Related Pages
* [Account Balances](/developers/account-balances) for available, locked, and total balances.
* [Perps Collateral](/developers/perps-collateral) for margin transfers and risk buckets.
* [TypeScript Vault](/sdk/typescript/vault) for full SDK method shapes.
# Earn
Source: https://docs.0xmonaco.com/earn
Earn from liquidity, referrals, applications, and proposed markets.
Monaco shares revenue with traders, builders, and market-proposers, accessible at all activity tiers, from retail trader to institutional market-maker.
## Ways To Earn
| Route | For | Reward |
| - | - | - |
| Maker Rebates | For traders providing active or passive liquidity. | A rebate on maker fills at every fee tier. |
| TraderCodes | Referring trading activity across multiple layers | A share of referral revenue from net protocol fees. |
| BuilderCodes | Building applications on top of Monaco's SDK and shared liquidity layer | Application fees and rewards from base protocol fees. |
| Market-Proposers | Proposing and actively launching markets | Revenue sharing, subject to liquidity and activity requirements. |
## Maker Rebates
Add liquidity through maker orders and receive a rebate when they fill. There is no minimum volume tier for access to maker rebates. See [Fees](/fees) for the current spot and perpetual schedules.
## TraderCodes
Every user can mint a unique TraderCode and use it to refer other traders. When someone signs up with your code, you earn a share of referral revenue from net protocol fees.
L1First-degree referralsL2Second-degree referralsL3+Deeper levels, up to 10
You earn from direct referrals and from the further layers of activity they bring, across up to 10 referral levels. Monaco sets how many levels earn and the reward rate for each level. These can be configured per client application, and a trade pays the levels of the client it was placed through. Rewards are allocated to your account **at the time of the trade**, and there is no limit on the number of traders you can invite.
See the [TraderCodes guide](/developers/tradercodes) for integration steps and account methods.
## BuilderCodes
Applications have two revenue routes:
1. **Application fees.** Add non-negative maker or taker fees on top of Monaco’s base fees and retain 100% of those app-specific fees.
2. **Protocol revenue rebates.** Earn rewards from base protocol fees in proportion to the volume routed to Monaco’s orderbook.
The [BuilderCodes guide](/developers/buildercodes) covers attribution, payout accounting, endpoints, and claim safety.
## Market-Proposers
Monaco’s market-proposer model lets proposers bring assets to market without a listing fee. A proposer supplies the minimum required liquidity and receives a **50% revenue share** from activity in that market while it remains listed and active.
The first instance of a proposed asset becomes the canonical version, avoiding multiple fragmented versions of the same asset.
Monaco protocol is designed to provide a seamless trading experience, while enabling participants on the ecosystem to earn directly from their contributions. Revenue-sharing is not limited to high-volume institutional programs or specific teams: all traders can earn through maker rebates and TraderCodes, and builders can monetize the applications they create.
* **Maker rebates:** All traders can earn maker rebates across all levels of volume activity. See [Fees](/fees) for current maker and taker fees.
* **TraderCodes:** A unified referral program that tracks activity and enables direct revenue-sharing.
* **BuilderCodes:** Earn on the trading activity routed through your chosen application.
* **Market proposers:** Proposers can earn from the activity generated by markets they launch on Monaco, once a market meets liquidity and activity requirements.
TraderCodes
Trade together, earn together. Every Monaco user will be able to mint a unique TraderCode, which they can use to refer other traders. When another user signs up with that code, the referrer earns a share of the referral revenue from net protocol fees.
Building with TraderCodes? See the [TraderCodes developer guide](/developers/tradercodes) for integration steps, SDK methods, and API reference.
Why share a Trader Code?
L1 first-degree referrals
L2 second-degree referrals
L3+ deeper referral levels, up to 10
* **Earn across up to 10 levels:** Users earn from the trading fees generated by direct referrals, but also activity from incremental referrals brought by those traders. Monaco configures how many levels earn, up to 10, and the reward rate for each level. These can be configured per client application, and a trade pays the levels of the client it was placed through.
* **Instantly accessible:** These rewards are allocated to your account at the time of the trade.
* **No limits to referrals:** Users can invite as many traders as they want.
BuilderCodes
Integrating a frontend? See the [BuilderCodes developer guide](/developers/buildercodes) for attribution, payout accounting, frontend endpoints, and claim safety.
There are two ways to earn:
* **Your fees, your pocket:** Frontends can add additional maker and taker fees (>= 0 bps) in addition to Monaco Protocol's base fees, and keep 100% of these app-specific fees.
* **Protocol revenue rebates:** Frontends earn additional rewards proportional to the volume they route to Monaco Protocol's order book, coming from base protocol fees.
Market Proposers
Monaco is designed to let proposers bring new assets to market without paying a listing fee. A proposer supplies the required minimum liquidity for the market and receives a 50% revenue share from activity generated in that market while the market remains listed and active.
Proposer markets are intended to avoid fragmented multiple versions of the same asset, and so the first instance of a proposed asset becomes the canonical version.
Proposer economics, minimum liquidity requirements, activity thresholds, and delisting rules are subject to Monaco governance and market-review requirements.
# Build a Trading App
Source: https://docs.0xmonaco.com/examples/build-trading-app
A step-by-step guide for frontend teams building a trading interface on Monaco Protocol's CLOB infrastructure
Monaco Protocol gives you a production-grade Central Limit Order Book (CLOB) as infrastructure. You bring the UI — Monaco handles order matching, on-chain settlement, and real-time data feeds.
This guide walks a frontend team through everything needed to ship a fully functional DEX on top of Monaco, from initial access to a live trading interface.
***
## Before You Start
Two things are required before any code gets written:
**1. Client ID** — identifies your application with Monaco. It routes all user activity — authentication, orders, fees, and data — to your specific application instance.
**2. User Access** — Public whitelist applications are closed. Existing approvals remain valid. Confirm access with the Monaco team before integrating.
Email [team@0xmonaco.com](mailto:team@0xmonaco.com) to get your Client ID and confirm whitelist access before integrating.
***
## Step 1: Install
```bash theme={null}
npm install @0xmonaco/core @0xmonaco/react @0xmonaco/types viem
```
`@0xmonaco/react` provides all React hooks. `@0xmonaco/types` exports TypeScript types for orders, events, and trading pairs. `viem` is required for wallet interactions and on-chain reads.
***
## Step 2: Wrap Your App with MonacoProvider
`MonacoProvider` initialises the SDK, opens the WebSocket connection, and makes every hook available throughout your component tree. It should wrap your entire app, inside your wallet provider.
```tsx theme={null}
import { MonacoProvider } from '@0xmonaco/react';
{children}
```
The `walletClient` comes from your wallet connection library of choice — wagmi, Thirdweb, or raw viem. The WebSocket connection opens automatically when the provider mounts, so public channels (orderbook, trades, OHLCV) are live immediately.
For production, source `seiRpcUrl` from an environment variable rather than hardcoding it.
→ [React SDK setup guide](/sdk/react/intro)
***
## Step 3: Connect Wallets & Authenticate
Monaco uses a challenge-response authentication flow. The user signs a short message with their wallet to authorize an ed25519 session key, which then signs every request. The `useAuth` hook handles the entire flow:
```tsx theme={null}
const { login, logout, authenticationStatus } = useAuth();
// Triggers a one-time wallet signature and starts a signed session
await login();
```
`authenticationStatus` will be `AuthenticationStatus.AUTHENTICATED`, `AuthenticationStatus.AUTHENTICATING`, or `AuthenticationStatus.UNAUTHENTICATED`. Use it to gate your trading UI and show a "Connect Wallet" button to unauthenticated users.
→ [React authentication hook](/sdk/react/hooks/use-auth)
***
## Step 4: Deposits
Users need to move tokens into Monaco's vault before they can trade. The `useVault` hook covers the full flow — ERC-20 approval, deposit, and withdrawal.
Token addresses and decimals should always be sourced from the trading pair object, not hardcoded:
```tsx theme={null}
import { parseUnits } from 'viem';
const { approve, deposit, withdraw, needsApproval } = useVault();
const { getTradingPairBySymbol } = useMarket();
const pair = await getTradingPairBySymbol('ETH/USDC');
if (!pair) throw new Error('Trading pair not found');
const assetId = pair.quoteAssetId; // Use baseAssetId for the other side
// Amounts must be bigint — use parseUnits with the pair's decimals
const amount = parseUnits('100', pair.quoteDecimals);
if (await needsApproval(assetId, amount)) {
await approve(assetId, amount);
}
await deposit(assetId, amount);
```
Use `useUserBalances` to reflect deposit confirmation in your UI the moment it lands — it subscribes to real-time balance updates internally, no polling required.
→ [Deposit guide](/deposit/deposit-tokens) · [Account Vault](/developers/vault) · [Withdraw](/deposit/withdraw)
***
## Step 5: Orderbook & Market Data
The orderbook streams via WebSocket — every update arrives in real time with no polling.
```tsx theme={null}
const { orderbook, subscribed } = useOrderbook(
pair.id, // Trading pair UUID
'SPOT', // Trading mode
1, // Magnitude: controls price-level grouping
'BASE', // Quotation mode: 'BASE' or 'QUOTE'
50 // Depth (number of price levels)
);
```
Use `calculateValidMagnitudes(midPrice)` from `@0xmonaco/core` to compute which magnitude steps are meaningful at the current price, and expose them as a selector so traders can zoom in or out.
For the price chart, use `useOHLCV` with intervals of `1m`, `5m`, `15m`, `1h`, `4h`, or `1d`. For a live recent-trades ticker, use `useTradeFeed(pair.id)` — it keeps the last 50 trades in state and updates in real time.
→ [WebSocket guide](/reference/websockets) · [OHLCV guide](/ohlcv)
***
## Step 6: Place & Manage Orders
```tsx theme={null}
const { placeLimitOrder, placeMarketOrder, cancelOrder } = useTrade();
// Limit order — rests on the book until filled or cancelled
await placeLimitOrder(pair.id, 'BUY', '0.5', '50000', { timeInForce: 'GTC' });
// Market order — executes immediately at the best available price
await placeMarketOrder(pair.id, 'SELL', '0.1');
```
**Time-in-Force options (limit orders only):**
| Option | Behaviour |
| - | - |
| `GTC` *(default)* | Rests on the book until filled or manually cancelled |
| `IOC` | Fills immediately, cancels any unfilled remainder |
| `FOK` | Fills completely or rejects the entire order |
Before submission, call `useFees` to show users a live fee estimate — this removes uncertainty and is one of the highest-impact UX improvements you can make.
For real-time order status, use `useUserOrders` — it fetches initial orders and subscribes to WebSocket updates automatically. You will receive lifecycle events — `OrderPlaced`, `OrderPartiallyFilled`, `OrderFilled`, `OrderCancelled`, `OrderRejected` — for every order the authenticated user places.
→ [Order Management](/developers/trade) · [useTrade](/sdk/react/hooks/use-trade)
***
## Step 7: Portfolio
The portfolio section ties together live balances, open orders, and a full activity history.
| Hook | What it provides |
| - | - |
| `useUserBalances` | Live vault balances (available, locked, total) per token — updates via WebSocket |
| `useUserOrders` | Paginated list of the user's orders with status and fill details |
| `useUserMovements` | Full ledger of deposits, withdrawals, trades, and fees |
`useUserBalances` subscribes to `ws.balances()` internally, so balances stay current across order placements, fills, deposits, and withdrawals without any extra wiring.
→ [Account Vault](/developers/vault) · [useUserOrders](/sdk/react/hooks/use-user-orders) · [useUserMovements](/sdk/react/hooks/use-user-movements)
***
## What to Build Next
Deep-dive into the wallet challenge flow, session keys, and request signing
All available channels — orders, orderbook, OHLCV, trades, balances, movements
Complete reference for every hook in @0xmonaco/react
REST endpoints for use cases that go beyond the SDK
# Complete Integration Example
Source: https://docs.0xmonaco.com/examples/complete-flow
End-to-end example showing authentication, deposits, withdrawals, and trading with Monaco Protocol SDK
This example demonstrates a complete integration workflow using Monaco Protocol SDK's core features: authentication, deposits, withdrawals, and trading.
## Installation
```bash theme={null}
npm install @0xmonaco/core viem
```
## Complete Example
```typescript theme={null}
import { MonacoSDK } from '@0xmonaco/core';
import { createWalletClient, custom, parseUnits } from 'viem';
import { seiTestnet } from 'viem/chains';
async function completeMonacoIntegration() {
try {
// 1. Request account access
const [account] = await window.ethereum.request({
method: 'eth_requestAccounts'
});
// 2. Initialize SDK with wallet
const walletClient = createWalletClient({
account,
chain: seiTestnet,
transport: custom(window.ethereum)
});
const sdk = new MonacoSDK({
walletClient,
network: 'staging',
seiRpcUrl: 'https://evm-rpc-testnet.sei-apis.com', // public testnet RPC — use a private provider in production
});
// 3. Authenticate (registers a session key; every authenticated request is signed).
// Pass connectWebSocket: true to also open the realtime stream (off by default).
console.log('Authenticating...');
const authState = await sdk.login('your-client-id', { connectWebSocket: true });
console.log('Authenticated as:', authState.user.address);
// 4. Get trading pair to access asset IDs
// Asset IDs come from trading pairs, not balances
const usdcPair = await sdk.market.getTradingPairBySymbol('ETH/USDC');
if (!usdcPair) {
throw new Error('Trading pair not found');
}
// For USDC (the quote token in ETH/USDC):
const assetId = usdcPair.quoteAssetId; // All vault operations use assetId
const decimals = 6; // USDC decimals
// 5. Check token balance
const balance = await sdk.profile.getUserBalanceByAssetId(assetId);
console.log('Available balance:', balance.availableBalance, balance.symbol);
console.log('Locked balance:', balance.lockedBalance, balance.symbol);
// 6. Deposit tokens (if you have them)
const depositAmount = parseUnits('10', decimals);
// Check if approval is needed
const needsApproval = await sdk.vault.needsApproval(assetId, depositAmount);
if (needsApproval) {
console.log('Approving tokens...');
const approvalResult = await sdk.vault.approve(assetId, depositAmount);
console.log('Approval transaction:', approvalResult.hash);
}
// Deposit tokens
console.log('Depositing tokens...');
const depositResult = await sdk.vault.deposit(assetId, depositAmount);
console.log('Deposit transaction:', depositResult.hash);
// 7. Check updated balance
const newBalance = await sdk.profile.getUserBalanceByAssetId(assetId);
console.log('Available balance:', newBalance.availableBalance, newBalance.symbol);
console.log('Total balance:', newBalance.totalBalance, newBalance.symbol);
// 8. Withdraw tokens (optional)
const withdrawAmount = parseUnits('5', decimals);
console.log('Withdrawing tokens...');
const withdrawResult = await sdk.vault.withdraw(assetId, withdrawAmount);
console.log('Withdrawal transaction:', withdrawResult.hash);
// Check balance after withdrawal
const balanceAfterWithdraw = await sdk.profile.getUserBalanceByAssetId(assetId);
console.log('Available balance:', balanceAfterWithdraw.availableBalance, balanceAfterWithdraw.symbol);
console.log('Total balance:', balanceAfterWithdraw.totalBalance, balanceAfterWithdraw.symbol);
// 9. Get trading pair (reuse the same one for trading)
const tradingPair = usdcPair; // ETH/USDC from step 4
// 10. Place a trading order
console.log('Placing limit order...');
const limitOrder = await sdk.trading.placeLimitOrder(
tradingPair.id, // Trading pair UUID
'BUY', // Side
'0.1', // Quantity (0.1 ETH)
'2000' // Price ($2000)
);
console.log('Order placed:', limitOrder.orderId);
// 11. Check order status (optional)
console.log('Order details:', limitOrder);
} catch (error) {
console.error('Error:', error.message);
// Handle specific error types
if (error.statusCode === 401) {
console.log('Authentication failed - check client ID');
} else if (error.statusCode === 403) {
console.log('Session expired or unauthorized - need to re-authenticate');
} else {
console.log('Other error:', error);
}
}
}
// Run the complete integration
completeMonacoIntegration();
```
## React Hook Example
For React applications, here's a reusable hook:
```typescript theme={null}
import { useState, useEffect } from 'react';
import { MonacoSDK } from '@0xmonaco/core';
import { createWalletClient, custom } from 'viem';
import { seiTestnet } from 'viem/chains';
function useMonacoSDK(clientId: string) {
const [sdk, setSdk] = useState(null);
const [authState, setAuthState] = useState(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
// Initialize SDK
useEffect(() => {
const initSDK = async () => {
if (!window.ethereum) {
setError('No wallet detected');
return;
}
try {
// Request account access
const [account] = await window.ethereum.request({
method: 'eth_requestAccounts'
});
const walletClient = createWalletClient({
account,
chain: seiTestnet,
transport: custom(window.ethereum)
});
const monacoSDK = new MonacoSDK({
walletClient,
network: 'staging',
seiRpcUrl: 'https://evm-rpc-testnet.sei-apis.com',
});
setSdk(monacoSDK);
} catch (err) {
setError(err.message);
}
};
initSDK();
}, []);
// Authentication function
const login = async () => {
if (!sdk) return;
setLoading(true);
setError(null);
try {
const result = await sdk.login(clientId);
setAuthState(result);
return result;
} catch (err) {
setError(err.message);
throw err;
} finally {
setLoading(false);
}
};
return {
sdk,
authState,
loading,
error,
login,
isAuthenticated: !!authState
};
}
// Usage in component
function TradingApp() {
const { sdk, authState, loading, error, login, isAuthenticated } = useMonacoSDK('your-client-id');
return (
{!isAuthenticated ? (
) : (
Connected: {authState.user.address}
{/* Your trading interface here */}
)}
{error && Error: {error}
}
);
}
```
## Key Points
### **Authentication**
* Use `sdk.login(clientId)` - it registers a local session key and signs every authenticated request (public market reads are unsigned); there are no tokens to copy or refresh
* The WebSocket is NOT auto-connected by default — pass `sdk.login(clientId, { connectWebSocket: true })` (or connect explicitly) before using `sdk.ws.orders()` to subscribe
* Handle 401/403 status codes for authentication errors
### **Deposits & Withdrawals**
* **Get `assetId` from trading pairs** via `sdk.market.getTradingPairBySymbol()` - use `quoteAssetId` or `baseAssetId`
* **`assetId`** (UUID) is used for vault operations: `approve()`, `deposit()`, `withdraw()`, `getAllowance()`, and `needsApproval()`
* **Check balances** using `sdk.profile.getUserBalanceByAssetId(assetId)` for detailed balance info (available, locked, total)
* Always check `needsApproval()` before depositing
* Use `parseUnits()` with correct decimals for token amounts
* Vault operations require authentication
### **Trading**
* All amounts are strings (e.g., `'1.5'`, `'2000.50'`)
* Trading operations require authentication
* Orders return an `orderId` for tracking
* Time-In-Force (TIF) options: `'GTC'` (default), `'IOC'`, or `'FOK'`
* **GTC**: Order stays active until filled or canceled
* **IOC**: Fill immediately, cancel unfilled portion
* **FOK**: Fill completely or reject entirely
### **Error Handling**
* Check `error.statusCode` for HTTP errors (401, 403)
* All operations can throw `APIError`, `ContractError`, etc.
## Next Steps
This example covers the core SDK functionality. For detailed guides on each feature:
* [Authentication Guide](/sdk/typescript/auth) - Wallet-based authentication
* [Deposit Guide](/deposit/deposit-tokens) - Token deposits and withdrawals
* [Order Management](/developers/trade) - Order placement and management
* [REST API](/api-reference) - Direct REST API usage
Comprehensive example repositories with Vite and Next.js are in development for more complete project setups.
# Perp Trading Example
Source: https://docs.0xmonaco.com/examples/perp-flow
End-to-end example: deposit, fund a parent margin account, open a 5x long, attach TP/SL, and close
This example walks through the full perp lifecycle on Monaco using `@0xmonaco/core`: depositing collateral, funding a parent margin account, opening a leveraged position, attaching TP/SL conditional orders, and closing.
For concepts, see [Margin](/trading-mechanics/perps), [Account Vault](/developers/vault#deposit-directly-to-perps-collateral), and [Positions](/developers/positions). For the spot-only walkthrough, see [Complete Integration Example](/examples/complete-flow).
## Installation
```bash theme={null}
npm install @0xmonaco/core viem
```
## Complete Example
```typescript theme={null}
import { MonacoSDK } from "@0xmonaco/core";
import { createWalletClient, custom, parseUnits } from "viem";
import { seiTestnet } from "viem/chains";
async function perpFlowExample() {
// ---------------------------------------------------------------------------
// 1. Connect wallet and initialize SDK
// ---------------------------------------------------------------------------
const [walletAccount] = await window.ethereum.request({
method: "eth_requestAccounts",
});
const walletClient = createWalletClient({
account: walletAccount,
chain: seiTestnet,
transport: custom(window.ethereum),
});
const sdk = new MonacoSDK({
walletClient,
network: "staging",
seiRpcUrl: "https://evm-rpc-testnet.sei-apis.com", // public testnet RPC — use a private provider in production
});
// ---------------------------------------------------------------------------
// 2. Authenticate
// ---------------------------------------------------------------------------
const auth = await sdk.login("your-client-id");
console.log("Authenticated as", auth.user.address);
// ---------------------------------------------------------------------------
// 3. Deposit USDC into Monaco's vault (skip if collateral is already on-chain)
// ---------------------------------------------------------------------------
// Get USDC asset metadata from any pair that uses it as quote
const ethUsdcPair = await sdk.market.getTradingPairBySymbol("ETH/USDC");
if (!ethUsdcPair) throw new Error("ETH/USDC pair not found");
const usdcAssetId = ethUsdcPair.quoteAssetId;
const usdcDecimals = ethUsdcPair.quoteDecimals;
// Approve and deposit 5,000 USDC into the vault
const depositAmount = parseUnits("5000", usdcDecimals);
if (await sdk.vault.needsApproval(usdcAssetId, depositAmount)) {
await sdk.vault.approve(usdcAssetId, depositAmount);
}
await sdk.vault.deposit(usdcAssetId, depositAmount);
// ---------------------------------------------------------------------------
// 4. Find a perp trading pair
// ---------------------------------------------------------------------------
// Perp pairs are distinguished by marketType === "MARGIN"
const { tradingPairs } = await sdk.market.getPaginatedTradingPairs({
pageSize: 100,
});
const perpPair = tradingPairs.find(
(p) => p.marketType === "MARGIN" && p.baseToken === "ETH",
);
if (!perpPair) throw new Error("No ETH perp pair available");
const tradingPairId = perpPair.id;
console.log(`Trading on ${perpPair.symbol} (${tradingPairId})`);
// ---------------------------------------------------------------------------
// 5. Fund the parent margin account with 1,000 USDC.
// ---------------------------------------------------------------------------
await sdk.marginAccounts.transferCollateralToParentMarginAccount({
asset: "USDC",
amount: "1000",
});
// Verify funding succeeded without storing a margin account id.
const summary = await sdk.marginAccounts.getParentMarginAccountSummary();
console.log(`Equity: ${summary.equity}, free: ${summary.freeCollateral}`);
// ---------------------------------------------------------------------------
// 6. Pre-flight the order with simulateRiskBucketOrderRisk
// ---------------------------------------------------------------------------
// Open a 5x long ETH at $2,000 with 1.5 ETH size
const orderQuantity = "1.5";
const limitPrice = "2000";
const leverage = "5";
const risk = await sdk.marginAccounts.simulateRiskBucketOrderRisk({
tradingPairId,
side: "BUY",
orderType: "LIMIT",
price: limitPrice,
quantity: orderQuantity,
leverage,
});
if (!risk.accepted) {
console.error("Order would be rejected:", risk.rejectReason);
return;
}
console.log(
`Estimated liquidation: ${risk.estimatedLiquidationPrice}, ` +
`free after: ${risk.freeCollateralAfter}`,
);
// ---------------------------------------------------------------------------
// 7. Place the perp order
// ---------------------------------------------------------------------------
const order = await sdk.trading.placeLimitOrder(
tradingPairId,
"BUY",
orderQuantity,
limitPrice,
{
tradingMode: "MARGIN",
leverage,
},
);
console.log(`Order ${order.orderId} submitted`);
// ---------------------------------------------------------------------------
// 8. Wait for the position to open via websocket
// ---------------------------------------------------------------------------
await sdk.ws.connect();
const positionId: string = await new Promise((resolve, reject) => {
const timeout = setTimeout(
() => reject(new Error("Order didn't fill in 30s")),
30_000,
);
const unsubscribe = sdk.ws.userOrders((evt) => {
if (evt.orderId !== order.orderId) return;
if (evt.eventType === "OrderFilled" || evt.eventType === "OrderPartiallyFilled") {
clearTimeout(timeout);
unsubscribe();
// Position lookup happens after a small settle delay
setTimeout(async () => {
const { positions } = await sdk.positions.listPositions({
tradingPairId: tradingPairId,
status: "OPEN",
});
const pos = positions[0];
if (!pos) reject(new Error("Position not found after fill"));
else resolve(pos.positionId);
}, 500);
} else if (evt.eventType === "OrderRejected" || evt.eventType === "OrderCancelled") {
clearTimeout(timeout);
unsubscribe();
reject(new Error(`Order ${evt.eventType}: ${(evt.data as any).reason}`));
}
});
});
console.log(`Position opened: ${positionId}`);
// ---------------------------------------------------------------------------
// 9. Inspect live risk
// ---------------------------------------------------------------------------
const positionRisk = await sdk.positions.getPositionRisk(positionId);
console.log({
mark: positionRisk.markPrice,
liquidation: positionRisk.liquidationPrice || "unavailable",
pnl: positionRisk.unrealizedPnl,
marginRatio: positionRisk.marginRatio,
});
// ---------------------------------------------------------------------------
// 10. Attach TP and SL (OCO group)
// ---------------------------------------------------------------------------
const tpSl = await sdk.positions.attachPositionTpSl(positionId, {
takeProfit: { triggerPrice: "2200", orderType: "MARKET" },
stopLoss: { triggerPrice: "1900", orderType: "MARKET" },
oco: true,
});
console.log(
`TP: ${tpSl.takeProfitOrderId}, SL: ${tpSl.stopLossOrderId}`,
);
// Subscribe to conditional-order lifecycle (optional — useful for UI updates)
const unsubConditionals = sdk.ws.conditionalOrders((evt) => {
console.log(
`Conditional ${evt.data.conditionalOrderId} → ${evt.data.state} ` +
`(${evt.data.reason})`,
);
});
// ---------------------------------------------------------------------------
// 11. Manual close (if you don't want to wait for TP/SL)
// ---------------------------------------------------------------------------
// Half close at limit
await sdk.positions.closePosition(positionId, {
closeType: "LIMIT",
limitPrice: "2050",
quantity: "0.75",
});
// Or full market close
// await sdk.positions.closePosition(positionId, { closeType: "MARKET" });
// ---------------------------------------------------------------------------
// 12. Cleanup
// ---------------------------------------------------------------------------
unsubConditionals();
sdk.ws.disconnect();
}
perpFlowExample().catch(console.error);
```
## What this example covers
1. **Deposit** — using the standard spot vault flow; collateral has to land on Monaco before it can be transferred to a margin account.
2. **Trading pair lookup** — perps are filtered by `marketType === "MARGIN"`.
3. **Parent margin account funding** — one collateral asset per account; USDC is the default.
4. **Preflight via `simulateRiskBucketOrderRisk`** — never place a non-trivial perp order without checking acceptance and projected liquidation.
5. **Order placement with perp options** — `tradingMode: "MARGIN"` and `leverage` are the key perp options; `side` sets the one-way direction.
6. **Position lookup after fill** — order events stream over `userOrders`; the position itself appears in `listPositions` shortly after.
7. **Live risk inspection** — `getPositionRisk` is the right call for live UI updates (cheaper than `getPosition`).
8. **TP/SL via `attachPositionTpSl`** — pass `oco: true` so the two legs cancel each other on trigger.
9. **Conditional order lifecycle subscription** — `sdk.ws.conditionalOrders` streams TP/SL state changes.
10. **Close** — partial or full, MARKET or LIMIT.
## Common variations
* **Reduce-only manual close.** Instead of `closePosition`, use `placeMarketOrder` with `reduceOnly: true` — useful when you want to apply slippage protection or specific TIF.
* **Withdraw collateral.** After closing all positions, use `transferCollateralFromParentMarginAccount` to move USDC back to the wallet, then `sdk.vault.withdraw(...)` to leave Monaco entirely.
## Reference links
* [Margin](/trading-mechanics/perps)
* [Account Vault](/developers/vault#deposit-directly-to-perps-collateral)
* [Positions](/developers/positions)
* [Margin Accounts SDK reference](/sdk/typescript/margin-accounts)
* [Positions SDK reference](/sdk/typescript/positions)
* [WebSockets reference](/sdk/typescript/websockets)
* [v0.7.0 release notes](/releases/v0.7.0)
# Fees
Source: https://docs.0xmonaco.com/fees
Base trading fees, maker rebates, and application charges.
Monaco uses maker and taker fees. **Maker fills add liquidity and receive a rebate at every fee tier. Taker fills remove liquidity and pay a fee.**
A limit order may fill immediately against existing liquidity, so placing a limit does not by itself make a fill a maker fill.
## Base Trading Fees
Illustrative tier-1 perpetual fill · before application fees\$10,000 taker fill → \$4.50 feeAt 4.50 bps, the fee is \$10,000 × 0.045%. At the same notional, the −0.10 bps maker rate gives a \$0.10 rebate.
## How Your Tier Is Calculated
Spot and perpetual volume are combined over the trailing **14 days**, with spot volume weighted by **2.5**:
Effective volume = 14-day perps volume + 2.5 × 14-day spot volume
Volume is counted per wallet across every application you trade through. That combined total sets your tier on every application.
Monaco can also set a volume floor on a wallet. Your tier then resolves from the higher of your weighted volume and that floor, while the volumes reported for your account stay as recorded, so a wallet with a floor can show a tier above what its weighted volume alone reaches.
Tiers are recalculated once per day at **00:00 UTC**.
## Application-Owned Fees
Applications built on Monaco can add their own maker or taker fees on top of the base fees and retain **100%** of those additional fees.
BuilderCodes separately attribute taker-side order flow to an application. Enrolled frontends receive a share of base protocol fees per fill. The revenue-share target and the TraderCodes referral levels and rates can be configured per client application, and a trade pays by the settings of the client it was placed through. See [BuilderCodes](/developers/buildercodes) for the revenue share and payout accounting.
Fee simulations let applications show estimated costs before order submission. Use the application’s fee preview to review the estimated charges for your order.
Traders adding liquidity into the order book through maker orders receive a rebate with every fill, while takers pay a competitive fee for execution. Every fee tier includes a maker rebate, which is available to retail and institutional market participants, rather than being limited to designated market maker programs or only the highest-volume accounts.
Fee Tiers
Monaco Protocol uses a maker-taker fee structure, tailored to the most active participants. Fee tiers are determined based on a combined trailing 14-day volume, counted per wallet: volume from every application you trade through adds up to one total, and that total sets your tier on every application.
```text theme={null}
Volume = (14d Perp Volume) + 2.5 x (14d Spot Volume)
```
Fee tiers are recalculated once per day at 00:00 UTC from trailing 14-day volume.
Spot Fees
Tier
Volume
Maker (bps)
Taker (bps)
1 -0.10 6.50
2 -0.20 6.00
3 -0.30 5.00
4 -0.40 4.00
5 -0.50 3.00
6 -1.00 2.50
Perpetual Futures Fees
Tier
Volume
Maker (bps)
Taker (bps)
1 -0.10 4.50
2 -0.20 4.25
3 -0.30 4.00
4 -0.40 3.50
5 -0.50 3.00
6 -1.00 2.50
This base fee structure is subject to change by protocol vote.
Application-Owned Fees
Builders wanting to build on top of Monaco Protocol's infrastructure can add their own maker or taker fees on top of Monaco's base fees. This lets frontends and tailored trading experiences create a direct revenue stream from the volume they route, and own 100% of the fees they charge on top of Monaco's base fees.
In addition to this, Monaco uses BuilderCodes to attribute taker-side routed order flow to an application, and a portion of its base fees is shared with enrolled frontends per fill. The revenue-share target and the TraderCodes referral levels and rates can be configured per client application, and a trade pays by the settings of the client it was placed through. See the [BuilderCodes developer guide](/developers/buildercodes) for the revenue share and RAW-unit payout semantics.
Use the fee simulation endpoints and SDK helpers before order submission so users can see estimated costs before placing a trade.
# Protobuf Reference
Source: https://docs.0xmonaco.com/grpc/protos
Packages, service groups, and protobuf entry points
Monaco's public gRPC API is defined by protobuf files in `protos/api/` and published through BSR as `buf.build/monaco-sdk/api`.
## Module
```text theme={null}
buf.build/monaco-sdk/api:main
```
## Package Layout
Each API service has its own package under `monaco.api.`. Shared messages live under `monaco.common.*`.
```protobuf theme={null}
package monaco.api.auth; // auth.proto
package monaco.api.orders; // orders.proto
package monaco.api.accounts; // accounts.proto
package monaco.api.market; // market.proto
package monaco.api.orderbook; // orderbook.proto
package monaco.api.trades; // trades.proto
package monaco.api.fees; // fees.proto
package monaco.api.applications; // applications.proto
package monaco.api.withdrawals; // withdrawals.proto
package monaco.api.delegated_agents; // delegated_agents.proto
package monaco.api.faucet; // faucet.proto
package monaco.api.health; // health.proto
package monaco.api.margin_accounts; // margin_accounts.proto
package monaco.api.positions; // positions.proto
package monaco.api.whitelist; // whitelist.proto
```
## Core
Wallet auth, refresh, and revoke.
Profiles, balances, movements, sub-accounts, trades, and portfolio data.
Application config, users, balances, orders, movements, and stats.
Public service health checks.
Retired public whitelist endpoint.
## Trading
Trading pairs, candles, metadata, screener, funding, open interest, and perp market data.
Order entry, cancellation, replacement, batch operations, and order lookup.
Public orderbook snapshots.
Public trade history and authenticated user trade history.
Fee simulation.
## Perps
Margin account summaries, collateral transfers, movements, and risk simulation.
Position listing, close flows, risk, margin adjustments, history, and TP/SL.
## Vault and Delegation
Withdrawal allocation and signed calldata lookup.
Delegated-agent registration, lookup, revocation, and delegated sessions.
Testnet token minting.
## Common Packages
Shared messages are imported by service packages and live under `protos/common/`.
```protobuf theme={null}
package monaco.common.trade;
package monaco.common.orders;
package monaco.common.user;
package monaco.common.balances;
package monaco.common.movements;
package monaco.common.sub_account;
package monaco.common.ohlcv;
package monaco.common.orderbook;
package monaco.common.faucet;
package monaco.common.enums;
```
For generated clients, start from the [gRPC API quickstart](/reference/grpc).
# AccountsService
Source: https://docs.0xmonaco.com/grpc/protos/accounts
Profiles, balances, movements, sub-accounts, trades, and portfolio RPCs
Package: `monaco.api.accounts`
Source: `protos/api/accounts.proto`
Use `AccountsService` for authenticated account state, balances, movements, portfolio analytics, funding payments, and sub-account limits.
```protobuf theme={null}
service AccountsService {
rpc GetProfile(GetProfileRequest) returns (GetProfileResponse);
rpc GetBalances(GetBalancesRequest) returns (GetBalancesResponse);
rpc GetBalanceByAsset(GetBalanceByAssetRequest) returns (GetBalanceByAssetResponse);
rpc GetMovements(GetMovementsRequest) returns (GetMovementsResponse);
rpc ListFundingPayments(ListFundingPaymentsRequest) returns (ListFundingPaymentsResponse);
rpc ListSubAccounts(ListSubAccountsRequest) returns (ListSubAccountsResponse);
rpc CreateLimit(CreateLimitRequest) returns (CreateLimitResponse);
rpc UpdateLimit(UpdateLimitRequest) returns (UpdateLimitResponse);
rpc GetLimits(GetLimitsRequest) returns (GetLimitsResponse);
rpc GetUserTrades(GetUserTradesRequest) returns (GetUserTradesResponse);
rpc GetPortfolioStats(GetPortfolioStatsRequest) returns (GetPortfolioStatsResponse);
rpc GetPortfolioChart(GetPortfolioChartRequest) returns (GetPortfolioChartResponse);
rpc GetPortfolioSummary(GetPortfolioSummaryRequest) returns (GetPortfolioSummaryResponse);
rpc GetPortfolioPerformance(GetPortfolioPerformanceRequest) returns (GetPortfolioPerformanceResponse);
rpc GetPortfolioRealizedPnl(GetPortfolioRealizedPnlRequest) returns (GetPortfolioRealizedPnlResponse);
rpc GetPortfolioAssets(GetPortfolioAssetsRequest) returns (GetPortfolioAssetsResponse);
rpc GetPortfolioMargin(GetPortfolioMarginRequest) returns (GetPortfolioMarginResponse);
rpc DeleteLimit(DeleteLimitRequest) returns (DeleteLimitResponse);
rpc GetSelfTradePreventionDefault(GetSelfTradePreventionDefaultRequest) returns (SelfTradePreventionDefaultResponse);
rpc SetSelfTradePreventionDefault(SetSelfTradePreventionDefaultRequest) returns (SelfTradePreventionDefaultResponse);
}
```
## Notes
* Most RPCs are user-authenticated.
* Sub-account limit RPCs are for master/sub-account permission management.
* User trade history is also available through `TradesService.ListUserTrades`.
* `GetPortfolioMargin` describes the default margin account unless `GetPortfolioMarginRequest.margin_account_id` names another of the caller's margin accounts (a sub-account), in which case `total`, `positions` and `risk_buckets` are that account's own. `GetPortfolioMarginResponse.margin_account_id` names the account described and is absent before the caller has a margin account. With `include_accounts`, the response also carries `accounts` — one `PortfolioMarginAccount` (`margin_account_id`, `subaccount_key`, `total`) per open margin account, the default one first, then sub-accounts by key — and `owner_total`, their sum. A malformed `margin_account_id` is `INVALID_ARGUMENT`; one that is not the caller's is `NOT_FOUND`; past the read budget it is `RESOURCE_EXHAUSTED` with a `RetryInfo` detail, and `UNAVAILABLE` when portfolio data is unavailable.
* `GetUserTrades` returns `UserTrade.fee` as the total fee paid: for a taker, Monaco's taker fee plus the application's builder fee; for a maker, the maker fee (negative for a rebate). `monaco_fee` + `builder_fee` = `fee`, with RAW twins `monaco_fee_raw` and `builder_fee_raw` beside `fee_raw`; `builder_fee` is `"0"` for a maker.
* The self-trade-prevention RPCs read and set one default per **wallet family** — every account sharing a wallet address, across applications — not per `users.id`. `SelfTradePreventionDefaultResponse` carries `mode` and `is_custom`, where `is_custom` is `false` when the platform default `CANCEL_MAKER` is being inherited.
* `SetSelfTradePreventionDefaultRequest.mode` accepts `CANCEL_MAKER`, `CANCEL_TAKER`, `CANCEL_BOTH` or `SKIP`, case-insensitively on the wire. An unknown mode is `INVALID_ARGUMENT`.
* **Both** RPCs also return `INVALID_ARGUMENT` when the authenticated session's wallet is not a canonical EVM address (`0x` + 40 hex). The default is keyed by wallet, so a legacy non-EVM session has no wallet family for it to apply to — the shared handler validates the address before reading or writing.
* **Both** RPCs are denied to delegated-agent sessions with `PERMISSION_DENIED` — an agent can neither read nor set its owner's default.
* `GetSelfTradePreventionDefault` is classified as an authenticated **read**, so it draws the per-account read budget and can return `RESOURCE_EXHAUSTED` with a `RetryInfo` detail once enforcement is enabled. The setter is a write: it draws the per-account settings-write budget it shares with the copy trading lead trader and follow writes (about one write per second, burst 10) and returns `RESOURCE_EXHAUSTED` with a `RetryInfo` detail past it, enforced from the first request. The setter also returns `UNAVAILABLE` when the matching engine is unreachable or the sequenced write fails transiently. That is not proof the setting is unchanged — `DeadlineExceeded` and `Cancelled` map to it too, so the write may have committed before the response was lost. Re-read with the getter, or retry the same `mode`: the call sets an absolute value, so a replay cannot land a different one.
* The setter is sequenced through the matching engine and returns only after the durable append, so the wallet's next order already resolves the new mode. The getter serves an asynchronous projection, so a read issued immediately after a set can briefly lag the setter's own response.
* A per-order `self_trade_prevention_mode` on `OrdersService` always overrides this default.
* `GetBalances`, `GetMovements` and `ListFundingPayments` are cursor-paginated: send an empty `page_token` to start, then each response's `next_page_token` until it comes back empty; `page_size` is 1 to 1000. There is no `page` field: a stub generated from an older proto that still sets it is not rejected (protobuf discards the unknown field) and reads the first page on every call, so regenerate stubs and walk the token.
# ApplicationsService
Source: https://docs.0xmonaco.com/grpc/protos/applications
Application configuration, users, balances, orders, movements, and stats
Package: `monaco.api.applications`
Source: `protos/api/applications.proto`
Use `ApplicationsService` for application configuration and public, application-scoped reporting.
```protobuf theme={null}
service ApplicationsService {
rpc GetConfig(GetConfigRequest) returns (GetConfigResponse);
rpc ListAppOrders(ListAppOrdersRequest) returns (ListAppOrdersResponse);
rpc ListAppUsers(ListAppUsersRequest) returns (ListAppUsersResponse);
rpc ListAppMovements(ListAppMovementsRequest) returns (ListAppMovementsResponse);
rpc GetAppStats(GetAppStatsRequest) returns (GetAppStatsResponse);
rpc ListAppBalances(ListAppBalancesRequest) returns (ListAppBalancesResponse);
}
```
## Notes
* `GetConfig` is session-authenticated. The reporting RPCs are public: each selects its application by `client_id` and is rate limited per client IP.
* Use `GetConfig` to fetch application-level runtime configuration.
* `ListAppOrders`, `ListAppUsers`, `ListAppMovements` and `ListAppBalances` are cursor-paginated: send an empty `page_token` to start, then each response's `next_page_token` until it comes back empty; `page_size` is 1 to 1000. There is no `page` field: a stub generated from an older proto that still sets it is not rejected (protobuf discards the unknown field) and reads the first page on every call, so regenerate stubs and walk the token.
* A missing `client_id` is `INVALID_ARGUMENT` and an unknown or inactive one is `NOT_FOUND`. The five reporting RPCs share a per-client-IP budget of 60 requests per minute (burst 120) and return `RESOURCE_EXHAUSTED` with a `google.rpc.RetryInfo` delay past it.
# AuthService
Source: https://docs.0xmonaco.com/grpc/protos/auth
Wallet and session authentication RPCs
Package: `monaco.api.auth`
Source: `protos/api/auth.proto`
Use `AuthService` to create wallet challenges, verify wallet signatures, refresh sessions, and revoke sessions.
```protobuf theme={null}
service AuthService {
rpc Challenge(ChallengeRequest) returns (ChallengeResponse);
rpc Verify(VerifyRequest) returns (VerifyResponse);
rpc Refresh(RefreshRequest) returns (RefreshResponse);
rpc Revoke(RevokeRequest) returns (RevokeResponse);
}
```
## Notes
* `Challenge` binds the ed25519 session public key into the wallet-signed message. The challenge is single-use and authorizes exactly one keypair, once. It lives 5 minutes, or 30 minutes on a deployment where contract-signature login is enabled, because a multisig has to collect owner approvals before it can answer — that window is uniform for every address rather than eligibility-specific, so this unauthenticated RPC cannot be used to probe which addresses hold a BuilderCodes payout bucket. Read `expires_at` rather than assuming either value.
* `Verify` opens the signed session. There is no access token — the session is the registered public key. `signature` carries whatever the wallet produced and is bounded at 8192 decoded bytes: anything longer is refused as `UNAUTHENTICATED` rather than forwarded to the chain. The bound is far above any realistic owner set, and EOA signatures never approach it.
* `VerifyResponse` carries two signup signals alongside `expires_at` and `user`. Both are scoped per `(wallet, application)` — identity is created per application, so a wallet already registered on another application still sees them on its first sign-in to a new one. `is_new_user` is true when this verify created the user row (the wallet's first sign-in to this application). `referral_applied` is true when a TraderCodes referral relationship was recorded on this verify — a valid, non-self TraderCode presented on that first sign-in resolved to a known referrer. Both are false on every returning verify for the application. Use `is_new_user` to gate first-time UX and `referral_applied` to confirm a referral landed.
* `Challenge` and `Verify` share one rate budget per client address — 60 requests/min sustained, burst 120 by default, on public ingress — and return `RESOURCE_EXHAUSTED` over it once enforcement is on. Enforcement is gated by `auth_rate_limit_enforced`, which defaults off; until it flips, an over-budget request is admitted with a warn log rather than rejected. Both figures are deployment defaults, overridden by `AUTH_RATE_LIMIT_PER_MINUTE` / `AUTH_RATE_LIMIT_BURST`, so treat the environment's own budget as authoritative. The check runs before the signature, so a throttled request is never an authentication failure.
* `Verify` returns `ALREADY_EXISTS` ("resource already exists") when the session public key it is asked to bind is already registered — the key is stored under a unique constraint, and the violation maps to that status. Do not retry with the same keypair: generate a fresh session keypair and start a new challenge. The high-level `login()` flow generates one per call, so this only reaches callers driving `Challenge` / `Verify` themselves.
* Both RPCs also return `INVALID_ARGUMENT` for invalid request parameters (on `Verify`, that includes a nonce already used or expired), `PERMISSION_DENIED` when the origin is not allowed for this application, `NOT_FOUND` for an invalid `client_id` or nonce, and `INTERNAL` for a server-side failure.
* `Verify` returns `UNAVAILABLE` when contract-signature (EIP-1271) verification of an eligible address could not be completed: the chain did not answer, did not answer within the deadline, or the server was at its concurrent-verification capacity and declined to ask. It is retryable and is **not** a verdict on the signature — a rejected signature is `UNAUTHENTICATED`, and so is a wallet contract that rejects by reverting. An eligible address can see it even when it turns out to be an EOA, because the code lookup precedes the choice of verification path; addresses that are not eligible never reach the chain. A fourth trigger is unrelated to contract wallets: `Verify` reads private-beta feature-flag state before anything else, and a read that cannot be served surfaces as the same retryable `UNAVAILABLE`, for any address, ahead of any signature or eligibility work.
* Authenticated RPCs are signed per request over the prost-encoded body. See [authenticated gRPC](/reference/grpc#authenticate-requests) for the metadata names, canonical signing string, session expiry/refresh, and a runnable signed example, or [TypeScript authentication](/sdk/typescript/auth) for the same session-key model in the SDK.
# BuildercodeRewardsService
Source: https://docs.0xmonaco.com/grpc/protos/buildercodes
BuilderCodes frontend revenue-share earnings and payout claim
Package: `monaco.api.buildercodes`
Source: `protos/api/buildercodes.proto`
Use `BuildercodeRewardsService` to inspect a frontend's BuilderCodes program,
earnings, and reserved balances, then claim accrued revenue share into a
tradeable balance.
```protobuf theme={null}
service BuildercodeRewardsService {
rpc GetBuildercodesConfig(GetBuildercodesConfigRequest) returns (GetBuildercodesConfigResponse);
rpc SetBuilderFee(SetBuilderFeeRequest) returns (SetBuilderFeeResponse);
rpc ListBuilderFeeChanges(ListBuilderFeeChangesRequest) returns (ListBuilderFeeChangesResponse);
rpc ListBuildercodePayouts(ListBuildercodePayoutsRequest) returns (ListBuildercodePayoutsResponse);
rpc GetBuildercodePayoutSummary(GetBuildercodePayoutSummaryRequest) returns (GetBuildercodePayoutSummaryResponse);
rpc GetBuildercodeRewardsBalance(GetBuildercodeRewardsBalanceRequest) returns (GetBuildercodeRewardsBalanceResponse);
rpc ClaimBuildercodeRewards(ClaimBuildercodeRewardsRequest) returns (ClaimBuildercodeRewardsResponse);
}
```
## Notes
* BuilderCodes provides per-trade frontend payouts. An enrolled frontend can earn Monaco revenue share and can charge its own builder fee. Every positive builder fee charged to a taker is credited in full to the enrolled application's claimable payout balance with kind `BUILDER_FEE`; revenue-share credits use `REVENUE_SHARE`. A claim moves the reserved payout balance into a spendable trading balance.
* Every RPC requires authentication and rejects delegated sessions. No program switch gates reads or claims; `SetBuilderFee` needs the revenue share or builder fees switched on. `GetBuildercodesConfig`, `SetBuilderFee`, `ListBuilderFeeChanges`, `ListBuildercodePayouts`, and `GetBuildercodePayoutSummary` derive the application from the session and require the authenticated wallet to match its configured payout address; callers cannot select an application or payout wallet.
* `GetBuildercodesConfig` returns the authenticated application's UUID, enrollment and payout-address state, the live BuilderCodes enabled/budget values, and `builder_fee_enabled`, stored `builder_fee_bps`, effective `builder_fee_cap_bps`, `changes_remaining`, and `next_change_allowed_at`. The effective cap never exceeds 100 bps. `buildercodes_enabled` reports whether the revenue share is on; reads succeed either way.
* `SetBuilderFee` accepts an integer from 0 through 100; a real change must also be at or below `builder_fee_cap_bps`. The application must be enrolled with a payout address. At most two `BUILDER` changes are allowed in a rolling 24 hours; `ADMIN` changes do not count. Setting the current value succeeds without using a change, even when none remain or the live cap has fallen below that value. The fee may be set while `builder_fee_enabled` is false as long as the revenue share is on, and is refused while both are off.
* `SetBuilderFee` requires an explicit `bps` (`0` clears the fee). It returns `INVALID_ARGUMENT` for a missing value or a real change above the cap, `FAILED_PRECONDITION` when enrollment is incomplete after ownership passes or the rolling change limit is exhausted, `PERMISSION_DENIED` when both fee switches are off or the non-delegated frontend payout-wallet gate fails (including a missing payout address), `NOT_FOUND` when the session application is absent, `UNAUTHENTICATED` without valid authentication, and `INTERNAL` on a backend failure. Maintenance may return `UNAVAILABLE` before the write. A change-limit message includes the next allowed RFC 3339 time. Setting the stored value remains a no-op even if the live cap has fallen below it; a newly stored fee reaches order requests within seconds. Sequencer ordering is authoritative for the builder-fee gate and enrollment: an order processed before one of those sequenced changes uses the earlier state, and a later order uses the new state. Builder fees are charged on taker fills only, so a resting remainder that later fills as maker pays none. Server keys are not accepted.
* `ListBuilderFeeChanges` returns `BUILDER` and `ADMIN` changes newest first. `page_size` defaults to 20 and must be from 1 through 100; continue with the opaque `next_page_token`. `actor` is the payout wallet for a `BUILDER` row and the operator email for an `ADMIN` row.
* `ListBuilderFeeChanges` returns `INVALID_ARGUMENT` for an invalid page size or token; the shared authenticated-read errors below also apply. An empty `next_page_token` ends the walk.
* `ListBuildercodePayouts` returns per-trade credits for the authenticated application, newest first across persistor batches. It is cursor-paginated: send an empty `page_token` to start, then each response's `next_page_token` until it comes back empty; an empty token is definitive. `page_size` defaults to `20` and must be 1–1000; a value out of range is `INVALID_ARGUMENT`. There is no `page` field: a stub generated from an older proto that still sets it has the unknown field discarded and reads the first page on every call, so regenerate it. The token is pinned to the request's `kind` filter; a token reused with a different filter is `INVALID_ARGUMENT`. The walk orders on `(created_at, id)` descending, which never skips or repeats a credit but is not chronological inside one persistor batch: every credit a batch writes shares its `created_at`, and the `id` tie-break is random. Each row exposes the trade, payout address at credit time, token, RAW atomic-unit amount, applied basis points, RFC 3339 timestamp, and `kind`. For `BUILDER_FEE`, the applied rate is the normalized builder-fee rate charged to the taker. For `REVENUE_SHARE`, it is the realized rate for that fill and may be lower than the configured budget. The optional `kind` request field lists one kind only, and `total`/`total_pages` then count that kind; any other value is `INVALID_ARGUMENT`. Pagination counts are JSON numbers.
* `GetBuildercodesConfig`, `ListBuilderFeeChanges`, `ListBuildercodePayouts`, and `GetBuildercodePayoutSummary` return `PERMISSION_DENIED` when the session is delegated or the authenticated wallet is not the application's configured payout address, `NOT_FOUND` when the authenticated application cannot be found, `UNAUTHENTICATED` without a session, `RESOURCE_EXHAUSTED` when the account's read budget is exhausted and enforced (retry after `RetryInfo`), and `INTERNAL` on a backend read failure.
* `GetBuildercodePayoutSummary` returns a total credit count plus totals separated by token; RAW atomic-unit amounts from different tokens are never combined. `by_token` sums both payout kinds; `by_token_and_kind` splits the same totals by token and `kind`, and a token's rows add up to its `by_token` total. `last_credited_at` is absent/null and both lists are empty when no payouts exist.
* `GetBuildercodeRewardsBalance` derives the reserved reward bucket from the authenticated wallet, not the application payout address. It returns one entry per token with non-zero available RAW atomic-unit balance, or an empty list. Current enrollment is deliberately not required, so a former payout wallet can inspect and claim previously accrued funds.
* `ClaimBuildercodeRewards` requires authentication and moves the specified amount from the caller's payout bucket into the calling application's trading balance. The move is ledger-only — no on-chain transaction and no gas — and the credited funds are immediately tradeable. Taking the money off the exchange then follows the ordinary withdrawal flow.
* The payout bucket is resolved from the authenticated caller's own wallet address and never from the request, which carries only `token` and `amount`. A caller can therefore only claim their own accrued balance.
* Returns `FAILED_PRECONDITION` when the payout balance is insufficient for the requested amount, including when the wallet has never accrued a share — an empty bucket and a bucket that never existed report the same thing, because in both cases there is no balance to move.
* Returns `RESOURCE_EXHAUSTED` with a `RetryInfo` detail when the per-user movement budget the claim shares with withdrawals and collateral transfers is enforced and spent. It is refused before the claim reaches the sequencer, so nothing committed and it is safe to retry after the interval.
* Returns `PERMISSION_DENIED` when the session is a delegated agent. A delegated grant conveys only order-management authority, and this claim turns reserved balance into a tradeable one. No program switch gates the claim: a balance already credited stays claimable whether or not the revenue share is on.
* Returns `UNAVAILABLE` when the matching engine is unreachable, restarting, or slow to answer. Treat it as an **unknown** outcome rather than a rejection: the request 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 to make a retry safe. Do **not** retry automatically, and do not settle it with a balance read: an admitted claim can still be queued when the balance is read, so an unchanged balance does not prove the claim will not commit. Treat the claim as pending, re-check once the engine is healthy, and reconcile before issuing another.
* Two `UNAVAILABLE`s are **not** ambiguous, and both are tagged. A deliberate maintenance block is refused before the handler runs (`x-maintenance-blocked: true` metadata; REST `"error": "OPERATIONS_BLOCKED"`), and a sequencer admission rejection is refused before the command is queued (`OVERLOADED` in the status' error details; REST `"code": "OVERLOADED"`). Neither reached the sequencer, so both committed nothing and are safe to retry with backoff — an overload positively wants one. Only an untagged availability failure carries the unknown outcome.
* The payout wallet may be an **EOA or a smart-contract wallet**. An EOA answers Monaco's signature challenge directly; a multisig such as a Safe holds no private key and instead authenticates by **EIP-1271** contract signature, where Monaco asks the wallet contract whether it accepts the challenge. That path is narrow by design and is offered only to a wallet holding a BuilderCodes payout bucket, only while `contract_wallet_auth_enabled` is on, and only where the deployment has a chain RPC endpoint configured. While any of those is unmet, login is EOA-only and a contract payout wallet can call neither this endpoint nor the withdrawal flow that follows, leaving its accrued share unclaimable. Because a multisig collects owner approvals before it can answer, challenges stay valid for 30 minutes rather than the usual 5 whenever `contract_wallet_auth_enabled` is on — for **every** address, not only eligible ones, so the unauthenticated challenge response cannot be used to probe which addresses hold a payout bucket.
* Current enrollment is deliberately **not** required. Payout-address changes are not retroactive — future shares accrue to the new wallet while everything already accrued stays with the old one — so the previous wallet can still claim what it earned while it was the payout address. Requiring present-day enrollment would leave that balance unclaimable by anyone.
* `amount` is in RAW atomic units of `token`, and BuilderCodes shares accrue in the quote token of the traded pair.
# CopyTradingService
Source: https://docs.0xmonaco.com/grpc/protos/copy-trading
Copy trading: lead trader profiles, follows and the leaderboard
Package: `monaco.api.copy_trading`
Source: `protos/api/copy_trading.proto`
Use `CopyTradingService` to register as a lead trader, browse and follow lead traders, and manage your follows. A follow allocates collateral to a copy account (a cross risk bucket inside your margin account) and mirrors the leader's fills into it as market orders under your own account.
```protobuf theme={null}
service CopyTradingService {
rpc UpsertLeadTrader(UpsertLeadTraderRequest) returns (UpsertLeadTraderResponse);
rpc GetMyLeadTrader(GetMyLeadTraderRequest) returns (GetLeadTraderResponse);
rpc ListMyFollowers(ListMyFollowersRequest) returns (ListMyFollowersResponse);
rpc ListLeadTraders(ListLeadTradersRequest) returns (ListLeadTradersResponse);
rpc GetLeadTrader(GetLeadTraderRequest) returns (GetLeadTraderResponse);
rpc ListLeadTraderClosedTrades(ListLeadTraderClosedTradesRequest) returns (ListLeadTraderClosedTradesResponse);
rpc ListLeadTraderOpenPositions(ListLeadTraderOpenPositionsRequest) returns (ListLeadTraderOpenPositionsResponse);
rpc UpsertFollow(UpsertFollowRequest) returns (UpsertFollowResponse);
rpc StopFollow(StopFollowRequest) returns (StopFollowResponse);
rpc CloseFollowPositions(CloseFollowPositionsRequest) returns (CloseFollowPositionsResponse);
rpc CloseAllCopyPositions(CloseAllCopyPositionsRequest) returns (CloseAllCopyPositionsResponse);
rpc ListMyFollows(ListMyFollowsRequest) returns (ListMyFollowsResponse);
rpc GetFollow(GetFollowRequest) returns (GetFollowResponse);
rpc ListFollowLeaderPositions(ListFollowLeaderPositionsRequest) returns (ListFollowLeaderPositionsResponse);
rpc PreviewFollow(PreviewFollowRequest) returns (PreviewFollowResponse);
rpc AddFollowPositionMargin(AddFollowPositionMarginRequest) returns (AddFollowPositionMarginResponse);
}
```
## Notes
* `ListLeadTraders`, `GetLeadTrader` and `ListLeadTraderClosedTrades` are public (no auth). Every other RPC requires authentication and is scoped to the session's account.
* A leader is addressed by their custom TraderCode handle. The sequencer allows one lead profile per wallet (a second application's registration from the same wallet is refused with `INVALID_ARGUMENT`), so a handle names one leader; a leader without a custom handle is not publicly addressable and is not listed. Users appear by wallet address and a `display` string (the custom handle when claimed), never an internal id. `GetLeadTrader` returns `NOT_FOUND` for a suspended leader.
* `UpsertLeadTrader` registers you or edits your profile. A registration must meet the platform's eligibility rules (account equity, trading history, closed positions, and a claimed custom TraderCode handle); a refused registration returns `INVALID_ARGUMENT` naming the rule. The profile is replaced as sent: an omitted `bio` or `min_allocation` clears it, an omitted `status` means `ACTIVE`, and an omitted `share_bps` keeps your current share. Each call spends the per-account settings-write rate limit `UpsertFollow` shares (about one write per second, burst 10; `RESOURCE_EXHAUSTED` past it).
* `UpsertFollow` without `follow_id` creates a follow; with it, edits one of your follows. An edit takes the leader from the follow, so it keeps working after the leader releases their handle (`leader` may be empty; a handle naming a different leader is refused). `copy_existing` on a create also copies the leader's open positions into the new copy account; it applies only to a create (`INVALID_ARGUMENT` on an edit), and the follow keeps recording whether it copied at creation. A create and an edit each spend the per-account settings-write rate limit `UpsertLeadTrader` and the self-trade-prevention default share (about one write per second, burst 10; `RESOURCE_EXHAUSTED` past it).
* `PreviewFollow` shows what a follow with those terms would open if it set `copy_existing`, as totals only: `estimated_margin` (what the copies would reserve at placement) and `estimated_fee`, both rounded up to cents, plus `copied_positions`, `skipped_positions` and `truncated`. It names no market, side or reason: the caller does not follow the leader yet. It is read-only and has the same session and terms rules as `UpsertFollow`: `PERMISSION_DENIED` for delegated agent and sub-account sessions, `NOT_FOUND` for an unknown or suspended leader, `INVALID_ARGUMENT` for terms a follow would refuse, and `FAILED_PRECONDITION` while copy trading is off. It spends the order-risk preview budget and the engine's preview lane share. At most 64 positions are sized; `truncated` is set when more matched.
* `StopFollow` stops one of your own follows. It is an exit and is never refused for the state of the account or the platform: a banned or sub-account session can still stop its own follow, and it stays available during an operations block, when registrations and follows do not. Only three things apply: ownership, the refusal of a delegated agent session, and a per-account rate limit of its own (about one stop per second, burst 20; `RESOURCE_EXHAUSTED` past it), which closes and follow edits never spend.
* `CloseFollowPositions` closes every position in one of your follows' copy accounts at market (reduce-only, immediate-or-cancel) and cancels the resting orders and conditional orders (TP/SL) on its copy risk buckets (a TWAP cannot run there), keeping the follow: the leader's next trades keep mirroring. `CloseAllCopyPositions` does it for every live follow of yours, one batch per follow. Not a flatten guarantee: a thin or halted market can leave part of a position open (`positions_not_closed`), and the leader's next trade opens copies again. Both are exits with `StopFollow`'s rules: never gated by copy trading being off or an operations block, no order budget, and only a delegated agent session is refused (`PERMISSION_DENIED`). Both share a per-account rate limit (about one close per second, burst 5; `RESOURCE_EXHAUSTED` past it). Proceeds stay in the copy account. A follow that is not yours is `NOT_FOUND`; a stopped follow or one whose stop is in progress is `ALREADY_EXISTS` (HTTP 409, the gateway's conflict code; `CloseAllCopyPositions` lists stopping follows in `stopping_follow_ids`). A refused close is `INVALID_ARGUMENT` and one that cannot run right now `UNAVAILABLE`; `CloseAllCopyPositions` lists such a follow in `failed_follows` with a fixed reason and closes the others. The response comes once the batch is durably logged (close orders placed, positions still open); fills arrive on the order, position and copy streams.
* `AddFollowPositionMargin` adds margin to one of your copy positions. `AddFollowPositionMarginRequest` carries `follow_id` (the follow's UUID), `trading_pair_id` (the copy position's trading pair UUID) and `amount` (margin to add in quote units, a positive decimal string). It moves `amount` from the follow's copy account into the margin of its isolated copy position on that pair: the position's initial margin requirement rises by exactly `amount` at the current mark (its leverage drops to what that amount buys at this price), the copy account's remaining collateral falls by the same, and your margin account is never touched. It lowers the position's leverage and moves its liquidation price away; it opens no exposure, so it works while copy trading is switched off and on a `CLOSE_ONLY` follow. `AddFollowPositionMarginResponse` returns `follow_id`, `trading_pair_id`, `risk_bucket_id` (the copy position's isolated risk bucket), `position_id`, `position_margin` (the position's stored margin after the add, in entry-price terms: it rises by `amount × entry price ÷ mark`) and `copy_account_available` (what the copy account can still put into this position after the add). There is no margin reduction: a follow exits by being stopped. `PERMISSION_DENIED` for delegated agent and sub-account sessions; `NOT_FOUND` unless the follow is yours; `ALREADY_EXISTS` (the REST 409) while the follow is stopping or once it is stopped; `INVALID_ARGUMENT` with code `COPY_TRADING_REJECTED` in its `google.rpc.ErrorInfo` detail when the add is refused (no isolated copy position on the pair, the copy position is being liquidated or is in bad debt, or more than the copy account can put in); switching copy trading off does not block an add, which opens no exposure; `UNAVAILABLE` when the matching engine is unavailable, and during an operations block (a margin move, unlike `StopFollow`); `RESOURCE_EXHAUSTED` over the per-user movement budget withdrawals and collateral transfers share. It is not idempotent: after a timeout, read the follow before retrying, or the amount may be added twice.
* `ListLeadTraderOpenPositions` is visible only to the leader's current followers (a live follow in the session's application); anyone else gets `PERMISSION_DENIED`. It is addressed by handle, so once a leader releases their handle it answers `NOT_FOUND` even for current followers, who still see the follow through `ListMyFollows` and `GetFollow`, and the leader's positions through `ListFollowLeaderPositions`.
* `ListFollowLeaderPositions` lists the open positions of the leader one of your follows copies, taking the leader from the follow. `NOT_FOUND` unless the follow is yours; `PERMISSION_DENIED` unless it is live (`ACTIVE` or `CLOSE_ONLY`).
* `GetFollowResponse.copy_account_equity` is the copy account's equity at the engine's marks: its collateral plus the unrealized PnL of its positions. It is a balance, not trading PnL, and it is absent when the engine cannot be read.
* Delegated agent sessions and sub-account sessions cannot register or follow (`PERMISSION_DENIED`); a delegated agent session cannot stop a follow either. A follow or profile that is not yours returns `NOT_FOUND`.
* When copy trading is switched off, registrations and new follows return `FAILED_PRECONDITION`.
* Stats (`LeadTraderStats`) come from the leader's account PnL rollups over 7d, 30d, 90d and all-time. `pnl` is trading PnL (realized plus unrealized, net of fees and funding). `roi_pct` divides it by the highest base balance over the window (starting equity plus net deposits), so a withdrawal cannot inflate it; it is absent when that base is not positive. `max_drawdown_pct` is the largest peak-to-trough fall of cumulative PnL over the same base, sampled at the window's rollup interval (1h for 7d, 4h for 30d and 90d, 1d for all-time). `win_rate_pct` counts winning over winning plus losing closes. A risk bucket's cash balance never feeds these figures.
* `LeadTraderFollowSummary.allocated` is the collateral allocated by live follows (AUM as allocated, not marked to market). `aum_equity` is the same AUM marked: the summed equity of the live follows' copy accounts at the engine's marks, absent when it could not be read. `sort=aum` ranks by it, with leaders lacking it last.
* `ListLeadTraders` serves a leaderboard snapshot refreshed about once a minute (`as_of`); it lists `ACTIVE` leaders only, sorted highest first by `sort`, with leaders lacking a value for the metric last.
* Lists use cursor pagination: omit `page_token` for the first page, then send each response's `next_page_token` until it is empty. A token only works with the filters and sort that produced it (`INVALID_ARGUMENT` otherwise). There are no totals. Page size: up to 1000 on your own lists (`ListMyFollows`, `ListMyFollowers`), up to 100 on the public lists.
# DelegatedAgentsService
Source: https://docs.0xmonaco.com/grpc/protos/delegated-agents
Delegated-agent registration, lookup, revocation, and delegated sessions
Package: `monaco.api.delegated_agents`
Source: `protos/api/delegated_agents.proto`
Use `DelegatedAgentsService` when a master account authorizes another wallet to trade on its behalf.
```protobuf theme={null}
service DelegatedAgentsService {
rpc UpsertDelegatedAgent(UpsertDelegatedAgentRequest) returns (DelegatedAgent);
rpc ListDelegatedAgents(ListDelegatedAgentsRequest) returns (ListDelegatedAgentsResponse);
rpc ListDelegatedAgentOwners(ListDelegatedAgentOwnersRequest) returns (ListDelegatedAgentOwnersResponse);
rpc RevokeDelegatedAgent(RevokeDelegatedAgentRequest) returns (RevokeDelegatedAgentResponse);
rpc CreateDelegatedSession(CreateDelegatedSessionRequest) returns (CreateDelegatedSessionResponse);
}
```
## Notes
* Owner-side RPCs require the master account.
* `UpsertDelegatedAgent` rejects a configured `max_open_orders`; no concurrent open-order cap is enforced.
* An owner holds at most 10 active, unexpired agents; `UpsertDelegatedAgent` rejects another past that until one is revoked or expires.
* An omitted `expires_at` defaults to 14 days from now, also on an update; one more than 180 days ahead returns `INVALID_ARGUMENT`.
* `allowed_actions` accepts `CREATE_ORDER`, `CANCEL_ORDER`, `REPLACE_ORDER`, `READ`, and `MANAGE_RISK_BUCKETS`; any other value returns `INVALID_ARGUMENT`. Every delegated session may read the owner's data, so empty `allowed_actions` or only `READ` is a read-only key.
* Each listed scope must match: with markets and margin accounts both listed, an order needs a listed market and a listed account. An agent listing only margin accounts trades those accounts on any market; one listing only markets trades them in every margin account. Both empty deny all orders. Empty order-type and time-in-force lists are unrestricted.
* `ip_allowlist` holds up to 20 IP addresses or CIDR ranges; more, or an unparseable entry, returns `INVALID_ARGUMENT`. Empty allows any address. `DelegatedAgent.ip_allowlist` returns the entries normalized to CIDR form.
* Over REST JSON, `agentAddress`, `allowedActions`, and `allowedTradingPairIds` must be present. The other four policy arrays accept omission, `null`, or `[]`. Protobuf repeated fields default to empty lists; the same policy semantics apply.
* `CreateDelegatedSession` exchanges an agent session for an owner-scoped delegated session. It returns `PERMISSION_DENIED` when no active delegation exists for the owner, or when the delegation has an `ip_allowlist` that does not include the caller's address.
# FaucetService
Source: https://docs.0xmonaco.com/grpc/protos/faucet
Testnet token minting
Package: `monaco.api.faucet`
Source: `protos/api/faucet.proto`
Use `FaucetService` to mint testnet tokens and read the faucet quota for an authenticated user.
```protobuf theme={null}
service FaucetService {
rpc MintTokens(MintTokensRequest) returns (MintTokensResponse);
rpc GetFaucetInfo(GetFaucetInfoRequest) returns (GetFaucetInfoResponse);
}
```
## Notes
* Faucet access is for testnet workflows.
* The quota is per wallet and application: `MintTokens` enforces the calling application's quota, and `GetFaucetInfo` counts and lists only the calling application's requests.
* Both RPCs reject delegated-agent sessions with `PERMISSION_DENIED` before reading or changing the quota.
* `GetFaucetInfo` returns `UNAUTHENTICATED` without a session, `PERMISSION_DENIED` for a delegated-agent session, `RESOURCE_EXHAUSTED` when the read budget is exhausted (retry after `RetryInfo`), `INTERNAL` when the quota history cannot be read, and `UNAVAILABLE` when the quota configuration is unavailable (safe to retry).
* `MintTokensRequest` requires `turnstile_token`, a Cloudflare Turnstile captcha token — a non-optional field, listed in the message's `required` schema. Runtime enforcement is governed by the `faucet_captcha_enforced` flag, which is on by default and fails closed: while it is on, a mint carrying no token or an invalid one is rejected, and an unconfigured verifier makes the endpoint temporarily unavailable rather than minting unchecked. An operator can disable enforcement for an environment that has no Turnstile verifier, in which case the token is not consulted — so always solve the captcha and send one.
# FeesService
Source: https://docs.0xmonaco.com/grpc/protos/fees
Fee simulation
Package: `monaco.api.fees`
Source: `protos/api/fees.proto`
Use `FeesService` to simulate fees before placing an order and to look up your current fee tier.
```protobuf theme={null}
service FeesService {
rpc SimulateFees(SimulateFeesRequest) returns (SimulateFeesResponse);
rpc GetMyFeeTier(GetMyFeeTierRequest) returns (GetMyFeeTierResponse);
}
```
## Notes
* Fee simulation helps estimate trading costs before order entry.
* `SimulateFeesResponse` carries the tier-resolved rate in `monaco_taker_fee_bps_exact` and `monaco_maker_rebate_bps_exact` — decimal basis-points strings (e.g. `"6.5"`, negative for a rebate) equal to the exact rate the ledger charges the caller. The integer `monaco_taker_fee_bps` / `monaco_maker_rebate_bps`, plus the flat `maker_fee_bps` / `taker_fee_bps` on `TradingPairData` and the account response, are **deprecated**: an integer cannot represent fractional tiered rates, so they echo the pre-tiered pair-level rate. Use the exact fields or `GetMyFeeTier`.
* `GetMyFeeTier` returns the authenticated caller's resolved fee tier (1–6), rolling 14-day weighted/spot/perp volumes, the volume needed to reach the next tier, and the requested pair's six-row fee schedule (`FeeTierScheduleRow`). Fee tiers are recalculated daily from a trailing 14-day weighted volume (`14d perp + 2.5 × 14d spot`), summed across every application the wallet trades through. A volume floor set on the wallet raises the volume the tier resolves from, once a set or changed floor applies to the wallet's fills, usually within a minute, and likewise for an account the wallet opens in a new application (a removed or expired floor stops counting at once); the reported volumes stay as recorded.
# HealthService
Source: https://docs.0xmonaco.com/grpc/protos/health
Public gRPC health check
Package: `monaco.api.health`
Source: `protos/api/health.proto`
Use `HealthService` to check API availability.
```protobuf theme={null}
service HealthService {
rpc Check(PublicHealthCheckRequest) returns (PublicHealthCheckResponse);
}
```
## Notes
* This is public and does not require authentication.
# ManagedMarketsService
Source: https://docs.0xmonaco.com/grpc/protos/managed-markets
Fenced maker leases and server-bounded quote refreshes on operator-managed launches
Package: `monaco.api.managed_markets`
Source: `protos/api/managed_markets.proto`
Use `ManagedMarketsService` to drive the maker side of an operator-managed market launch: read the launches this wallet is assigned to, hold a short lease that fences one process at a time, and ask the engine to restate a quote set whose prices, sizes, fees and risk bucket are all chosen server-side. It never places a quote the caller describes and never moves funding.
```protobuf theme={null}
service ManagedMarketsService {
rpc ListAssignments(ListManagedMarketAssignmentsRequest) returns (ListManagedMarketAssignmentsResponse);
rpc ClaimLease(ClaimManagedMarketLeaseRequest) returns (ManagedMarketLease);
rpc RefreshQuotes(RefreshManagedMarketQuotesRequest) returns (ManagedMarketQuoteStatus);
}
```
## Notes
* All three RPCs require an authenticated **wallet** session, and a delegated-agent session is refused on every one of them with `PERMISSION_DENIED` (REST `403`) — permanently, not as a rollout state. The maker requirement is narrower: `ClaimLease` and `RefreshQuotes` are launch-scoped and need a session whose user *and* application both match that launch's configured maker, while `ListAssignments` accepts any authenticated wallet and filters by it, so a non-maker gets an empty page rather than an error.
* `owner_id` is a **process fence, never authorization**. It is a UUID the caller chooses to identify the process holding the lease; authorization is derived solely from the authenticated session's user and application binding and never from an identity in the request body, so no `owner_id` widens what a caller can reach. A value that is not a UUID returns `INVALID_ARGUMENT`, as does a non-UUID `launch_id` on `ClaimLease` or `RefreshQuotes`, a non-positive `quote_revision`, and `ListAssignments` pagination outside `page 1..=10000` or `page_size 1..=100`.
* **A different `owner_id` cannot seize a live lease.** `claim_maker` admits a claim only when the lease is unset, already expired, or held by that same owner, so a standby claiming under a new `owner_id` gets `ALREADY_EXISTS` (REST `409`) until the current lease lapses. Takeover is "wait out the remaining lease, then claim". A claim after expiry increments `maker_generation`, which is what makes the previous holder's next refresh fail as stale. A claim by the **current** owner splits on `expected_generation`: supplied, it renews through `renew_maker` and moves the expiry out; omitted while the lease is still unexpired, it neither claims nor renews — it replays the existing reservation and returns its original expiry, so a loop that always omits it will let its own lease lapse.
* A launch this session is not the configured maker for answers `NOT_FOUND`, not `PERMISSION_DENIED`, so the service never confirms the existence of a launch the caller has no part in. `ListAssignments` is the exception in shape rather than in rule: it filters by the caller, so a wallet with no assignments gets an **empty page** rather than any error. `PERMISSION_DENIED` is reserved for delegated-agent sessions.
* `generation` and `quote_revision` are `uint64` fences. Over gRPC they are `uint64` fields; over REST they are **exact decimal strings**. A JSON client should keep the wire value as text but must not order or increment it *as* text — `"10" < "2"` lexically — and must not round-trip it through a double, which loses values above 2^53. Convert to an exact 64-bit integer type (`BigInt` in JavaScript) for every comparison and increment.
* `ListAssignments` is cursor-paginated, oldest first: send an empty `page_token` to start, then each response's `next_page_token` until it comes back empty; an empty token is definitive. `page_size` defaults to `100` and must be 1–1000, and a larger value is rejected rather than clamped. The walk seeks on `(created_at, id)` ascending, so a launch assigned mid-walk cannot make it repeat or drop a row. There is no `page` field: a stub generated from an older proto that still sets it has the unknown field discarded and reads the first page on every call, so regenerate it.
* `ListAssignments` returns **expected inventory**, not a ready-to-quote set: a row with `quote_enabled = false` is still assigned and still listed, but `ClaimLease` and `RefreshQuotes` on it return `ALREADY_EXISTS` (REST `409`, `Managed market is not ready for maker quotes`) until the launch is ready to quote: its launch phase is `warming`, `activating` or `live`, its runtime phase is `warming` or `live`, and its allocated collateral has reached the configured allocation in full. **`activating` counts** — a maker must keep quoting through activation, which is the phase that depends on those quotes. Its `lease` field is **declared on the message but never populated**: the released handler serializes it as absent on every row, deliberately, because the database reservation cannot prove the engine's current generation or quote revision and a fabricated zero would read as a usable fence. `ClaimLease` is the only source of a lease a caller may act on.
* The two `phase` fields are **different vocabularies**. On `ManagedMarketAssignment` and `ManagedMarketLease` it is the launch phase: `pending`, `validating`, `registering`, `warming`, `activating`, `live` or `failed`. On `ManagedMarketQuoteStatus` it is the engine's runtime phase and takes only `warming`, `live` or `halted`; anything else the engine reports is answered as `UNAVAILABLE` with `Managed market status unavailable; reconcile the lease`.
* `ClaimLease` reserves the lease, fences it in the matching engine, then rechecks ownership before replying, so the reply is the engine's view rather than the database reservation — a lease that expired in flight fails instead of being reported as held. The lease runs for 30 seconds. `expected_generation` is required when renewing an unexpired lease you hold and omitted for an initial claim or after expiry.
* An initial claim whose reply was lost replays its existing durable reservation instead of incrementing `generation`, so a transport retry never costs you the lease you already hold.
* `RefreshQuotes` places one bounded bid and one bounded ask. `quote_revision` must be positive (`INVALID_ARGUMENT` otherwise) and at least the revision the engine holds for that generation: a **lower** one is refused, and re-sending the one it already holds is an accepted idempotent no-op. Derive it from the `ClaimLease` reply rather than starting at `1` — a renewal keeps the generation's existing revision, so `1` restates the quote set when the held revision is exactly `1` and is refused once it has advanced past it. Only a claim that bumped the generation resets the revision to `0`.
* The engine also enforces the launch's own cadence, refusing a refresh that *advances* the revision less than `refresh_interval_ms` after the last one. That is a rejection rather than a dropped no-op, so pace the loop on the assignment's `refresh_interval_ms`. An equal-revision retry is exempt — `quote_intent` returns on the equality check before it reaches the interval check — so an idempotent replay succeeds inside the window.
* **Two `ALREADY_EXISTS` paths, and the engine's own refusals are indistinguishable.** The handler re-checks the launch's persisted readiness *before* calling the engine, so a launch already recorded as halted or otherwise not quotable answers `Managed market is not ready for maker quotes`. Everything the engine itself refuses is folded by `handlers::managed_markets::engine_error` — `ABORTED`, `ALREADY_EXISTS`, `FAILED_PRECONDITION` and `INVALID_ARGUMENT` all become one `ALREADY_EXISTS` carrying `Managed market lease changed or expired; reconcile the lease` — so a stale revision, an early refresh, an expired lease and a market halted since the readiness check are one status and one message. Both paths are `ALREADY_EXISTS`: do not branch on the text, reconcile through `ClaimLease` and act on the generation and revision it returns.
* **Cost:** `ClaimLease` admits one item and `RefreshQuotes` two against the caller's ordinary order-creation budget, and `ListAssignments` draws the per-account **read** budget like every other authenticated read, charged before the handler runs. Those two budgets are the only sources of `RESOURCE_EXHAUSTED` here — the engine's cadence refusal is **not** one of them; it arrives as the folded `ALREADY_EXISTS` below. Either way it carries a `google.rpc.RetryInfo` detail — the same interval REST returns as `retryAfter` — so honor that rather than guessing. See [Rate limits](/developers/rate-limits).
* `ALREADY_EXISTS` (REST `409`) also covers `Managed market lease changed or expired; reconcile the lease`: the lease moved to another owner or generation, or lapsed. Reconcile through `ClaimLease` and read the engine-observed `generation` and `quote_revision` back before continuing.
* Treat a `RefreshQuotes` timeout or `UNAVAILABLE` as an **unknown outcome**, not a rejection: the intent may have reached the engine. Reconcile through `ClaimLease` and take the engine's `quote_revision` rather than advancing your own and retrying blind.
* `UNAVAILABLE` (REST `503`) means the matching engine could not be reached or answered with a status this service cannot act on. `INTERNAL` is a generic server-side failure and can happen before the engine is contacted at all, so unlike `UNAVAILABLE` it says nothing about engine state.
* Quote intents are server-derived end to end: a fixed preallocated risk bucket, server-chosen prices, sizes and the application's canonical taker fee. The request carries no price, quantity or identity that could override any of it, and no path here funds an account.
# MarginAccountsService
Source: https://docs.0xmonaco.com/grpc/protos/margin-accounts
Parent margin account summaries, risk bucket collateral transfers, movements, and risk simulation
Package: `monaco.api.margin_accounts`
Source: `protos/api/margin_accounts.proto`
Use `MarginAccountsService` for parent margin account state, risk bucket collateral movement, and pre-trade risk checks.
```protobuf theme={null}
service MarginAccountsService {
rpc GetMarginAccountSummary(GetMarginAccountSummaryRequest) returns (GetMarginAccountSummaryResponse);
rpc GetParentMarginAccountSummary(GetParentMarginAccountSummaryRequest) returns (GetMarginAccountSummaryResponse);
rpc ListMarginAccounts(ListMarginAccountsRequest) returns (ListMarginAccountsResponse);
rpc GetAvailableCollateral(GetAvailableCollateralRequest) returns (GetAvailableCollateralResponse);
rpc TransferCollateralToMarginAccount(TransferCollateralToMarginAccountRequest) returns (TransferCollateralToMarginAccountResponse);
rpc TransferCollateralFromMarginAccount(TransferCollateralFromMarginAccountRequest) returns (TransferCollateralFromMarginAccountResponse);
rpc TransferCollateralToParentMarginAccount(TransferCollateralToParentMarginAccountRequest) returns (TransferCollateralToMarginAccountResponse);
rpc TransferCollateralFromParentMarginAccount(TransferCollateralFromParentMarginAccountRequest) returns (TransferCollateralFromMarginAccountResponse);
rpc GetMarginAccountMovements(GetMarginAccountMovementsRequest) returns (GetMarginAccountMovementsResponse);
rpc GetParentMarginAccountMovements(GetParentMarginAccountMovementsRequest) returns (GetMarginAccountMovementsResponse);
rpc SimulateOrderRisk(SimulateOrderRiskRequest) returns (SimulateOrderRiskResponse);
rpc SimulateParentMarginOrderRisk(SimulateParentMarginOrderRiskRequest) returns (SimulateOrderRiskResponse);
rpc TransferCollateralToRiskBucket(TransferCollateralToRiskBucketRequest) returns (TransferCollateralToMarginAccountResponse);
rpc SimulateRiskBucketOrderRisk(SimulateRiskBucketOrderRiskRequest) returns (SimulateOrderRiskResponse);
rpc CreateSubAccount(CreateSubAccountRequest) returns (CreateSubAccountResponse);
rpc CloseSubAccount(CloseSubAccountRequest) returns (CloseSubAccountResponse);
rpc GetMarginAccountPnl(GetMarginAccountPnlRequest) returns (GetMarginAccountPnlResponse);
rpc TransferCollateral(TransferCollateralRequest) returns (TransferCollateralResponse);
rpc TransferCollateralToSubAccount(TransferCollateralToSubAccountRequest) returns (TransferCollateralResponse);
rpc TransferCollateralFromSubAccount(TransferCollateralFromSubAccountRequest) returns (TransferCollateralResponse);
}
```
## Notes
* Margin account RPCs are authenticated.
* Parent margin account RPCs operate on the authenticated user's parent account.
* Risk bucket RPCs resolve the target isolated bucket from request context such as trading pair and optional strategy key.
* `SimulateRiskBucketOrderRiskRequest.selected_trading_pair_ids` is **deprecated and ignored** in CROSS mode, the contract `TransferCollateralToRiskBucketRequest` already adopted for its own field. 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 — and the preview never priced anything from the list, so requiring it only produced `INVALID_ARGUMENT`s. A CROSS preview with an empty list, one omitting `trading_pair_id`, and one omitting a pair with an open cross position now all succeed, with unchanged figures. The field stays on the wire so pre-deprecation clients keep working; sending a value logs a server-side deprecation warning and has no other effect, and it is still rejected for ISOLATED (`selectedTradingPairIds is not supported for ISOLATED risk buckets`). `SimulateOrderRiskResponse.selected_trading_pair_ids` 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 — never the caller's list. The generated gRPC struct carries `#[deprecated]`, so build the request with `..Default::default()` rather than naming the field, which trips rustc's `deprecated` lint under `-D warnings`.
* `TransferCollateralFromMarginAccountRequest` accepts an optional `margin_mode` (`ISOLATED` or `CROSS`); set `CROSS` to release the account's cross risk bucket, which has no trading pair and cannot be named by `trading_pair_id`. It is mutually exclusive with `trading_pair_id`, and released collateral returns to the parent margin account.
* `MarginAccountSummary` carries `subaccount_key` (`default` for the default margin account) and `margin_account_state` (`ACTIVE` or `CLOSED`) on every row, and `risk_bucket_state` (`NORMAL`, `PENDING_LIQUIDATION`, `LIQUIDATING`, `BAD_DEBT`, `CLOSED`) on risk-bucket rows only. `ListMarginAccountsRequest.state` filters on `risk_bucket_state`, so any filter excludes margin account rows, which have no state.
* `CreateSubAccount` opens a margin sub-account keyed by `subaccount_key`: up to 64 letters, digits, `_`, `-`, `.` or `:`; keys starting with `copy:` are reserved and `default` names the default margin account. The default margin account must already exist. Re-creating an open key is idempotent; re-creating a closed key re-opens the same account. Open sub-accounts are capped per owner by weighted 14-day volume tier (10 at the first tier, rising with the fee-tier floors to 50) or an admin override; copy-trading sub-accounts do not count. Returns `INVALID_ARGUMENT` for a malformed, reserved or `default` key, a missing default margin account, or a create past the cap; `PERMISSION_DENIED` for delegated-agent sessions; `RESOURCE_EXHAUSTED` (with `RetryInfo`) past the per-user movement budget; `UNAVAILABLE` when no matching engine connection is configured or the new account has not been projected yet; and `INTERNAL` when the call to the matching engine fails in transit.
* `CloseSubAccount` closes a flat sub-account (no position, resting order, conditional order or TWAP) in one call: its flat, solvent risk buckets close and all of its remaining collateral moves to the default margin account. `CloseSubAccountResponse` names `margin_account_id`, `default_margin_account_id` and the `amount` moved. Returns `INVALID_ARGUMENT` for the default margin account, a copy-trading sub-account (closed by stopping its follow), an already-closed sub-account, a sub-account that is not flat, or one with a risk bucket in liquidation, in bad debt, or insolvent — nothing moves; `NOT_FOUND` for an account that is not the caller's; `PERMISSION_DENIED` for delegated-agent sessions; `RESOURCE_EXHAUSTED` past the movement budget; `UNAVAILABLE` when no matching engine connection is configured; `INTERNAL` when the call to the matching engine fails in transit.
* `GetMarginAccountPnl` returns one of the caller's margin accounts' own `realized_pnl` (settled on the account plus still held in its open risk buckets) and `unrealized_pnl` (open positions at the latest risk marks); the caller's accounts sum to the owner-wide figure. Returns `INVALID_ARGUMENT` for a malformed id, `NOT_FOUND` for an account that is not the caller's, and `RESOURCE_EXHAUSTED` past the read budget.
* `TransferCollateral` moves collateral between `from` and `to` `CollateralTransferEndpoint`s, each naming exactly one of `wallet` and `margin_account_id`. Served moves: wallet to the default margin account and back, and margin account to margin account. An endpoint carrying `risk_bucket_id` is refused with `INVALID_ARGUMENT` — fund risk buckets with `TransferCollateralToRiskBucket`. Also `INVALID_ARGUMENT` for wallet to wallet, a wallet transfer naming a sub-account, the same account on both sides, or a closed account; `NOT_FOUND` for an account that is not the caller's; `PERMISSION_DENIED` for delegated-agent sessions; `RESOURCE_EXHAUSTED` past the movement budget.
* `TransferCollateralToSubAccount` moves `amount` from the caller's default margin account to the sub-account `margin_account_id`; `TransferCollateralFromSubAccount` moves it back. Each is the same one-step account-to-account move as `TransferCollateral`. Returns `INVALID_ARGUMENT` when `margin_account_id` is the default margin account (use the parent-margin-account RPCs), the sub-account is closed, or the amount exceeds the source's free collateral; `NOT_FOUND` for an account that is not the caller's; `PERMISSION_DENIED` for delegated-agent sessions; `RESOURCE_EXHAUSTED` past the movement budget.
* `TransferCollateralToRiskBucketRequest.margin_account_id` and `SimulateRiskBucketOrderRiskRequest.margin_account_id` select the margin account: omit it for the default margin account, or name one of the caller's open sub-accounts to fund or preview that sub-account's own risk buckets. A closed sub-account or a copy-trading sub-account is `INVALID_ARGUMENT`; another user's account is `NOT_FOUND`.
* `ListMarginAccounts`, `GetMarginAccountMovements` and `GetParentMarginAccountMovements` are cursor-paginated: send an empty `page_token` to start, then each response's `next_page_token` until it comes back empty; `page_size` is 1 to 1000. There is no `page` field: a stub generated from an older proto that still sets it is not rejected (protobuf discards the unknown field) and reads the first page on every call, so regenerate stubs and walk the token.
# MarketService
Source: https://docs.0xmonaco.com/grpc/protos/market
Trading pairs, candles, metadata, screener, funding, and perp market data
Package: `monaco.api.market`
Source: `protos/api/market.proto`
Use `MarketService` for public market data and perp market metadata.
```protobuf theme={null}
service MarketService {
rpc ListTradingPairs(ListTradingPairsRequest) returns (ListTradingPairsResponse);
rpc GetTradingPair(GetTradingPairRequest) returns (GetTradingPairResponse);
rpc GetCandles(GetCandlesRequest) returns (GetCandlesResponse);
rpc GetMarketMetadata(GetMarketMetadataRequest) returns (GetMarketMetadataResponse);
rpc GetScreener(GetScreenerRequest) returns (GetScreenerResponse);
rpc GetOpenInterest(GetOpenInterestRequest) returns (GetOpenInterestResponse);
rpc GetFundingState(GetFundingStateRequest) returns (GetFundingStateResponse);
rpc ListFundingHistory(ListFundingHistoryRequest) returns (ListFundingHistoryResponse);
rpc ListAllFundingHistory(ListAllFundingHistoryRequest) returns (ListAllFundingHistoryResponse);
rpc GetPerpMarketConfig(GetPerpMarketConfigRequest) returns (GetPerpMarketConfigResponse);
rpc GetPerpMarketSummary(GetPerpMarketSummaryRequest) returns (GetPerpMarketSummaryResponse);
rpc GetMarkPrice(GetMarkPriceRequest) returns (GetMarkPriceResponse);
rpc GetIndexPrice(GetIndexPriceRequest) returns (GetIndexPriceResponse);
}
```
## Notes
* Market-data RPCs are public.
* Use `ListTradingPairs` to discover trading pair IDs before calling orderbook, trade, or order RPCs.
* An operator-managed market is excluded from `ListTradingPairs`, the pair detail reads, `GetScreener` and the public price RPCs until its durable runtime projection reaches live. `is_active` does not override that on either request: passing `false` surfaces delisted ordinary pairs, never a pending, warming or halted managed launch. A pair with no managed launch keeps its existing visibility.
* `ListFundingHistory` and `ListAllFundingHistory` are cursor-paginated: send an empty `page_token` to start, then each response's `next_page_token` until it comes back empty; `page_size` is 1 to 1000. There is no `page` field: a stub generated from an older proto that still sets it is not rejected (protobuf discards the unknown field) and reads the first page on every call, so regenerate stubs and walk the token.
* `TradingPairData.isolated_only` marks an isolated-only market: a `CROSS` order that is not reduce-only is `INVALID_ARGUMENT` with `ErrorInfo.reason` `CROSS_MARGIN_NOT_ALLOWED`.
# OrderbookService
Source: https://docs.0xmonaco.com/grpc/protos/orderbook
Public orderbook snapshots
Package: `monaco.api.orderbook`
Source: `protos/api/orderbook.proto`
Use `OrderbookService` for public orderbook snapshots.
```protobuf theme={null}
service OrderbookService {
rpc GetOrderbook(GetOrderbookRequest) returns (GetOrderbookResponse);
rpc GetOrderbookRPC(GetOrderbookRPCRequest) returns (GetOrderbookResponse);
}
```
## Notes
* `GetOrderbookRPC` is a gRPC-only wrapper over the same response type.
* Use WebSockets for live orderbook updates.
# OrdersService
Source: https://docs.0xmonaco.com/grpc/protos/orders
Order entry, cancellation, replacement, batch operations, and order lookup
Package: `monaco.api.orders`
Source: `protos/api/orders.proto`
Use `OrdersService` for authenticated order management.
```protobuf theme={null}
service OrdersService {
rpc CreateOrder(CreateOrderRequest) returns (CreateOrderResponse);
rpc CancelOrder(CancelOrderRequest) returns (CancelOrderResponse);
rpc BatchCancelOrders(BatchCancelOrdersRequest) returns (BatchCancelOrdersResponse);
rpc BatchCancelAll(BatchCancelAllRequest) returns (BatchCancelAllResponse);
rpc BatchCancelAllByPair(BatchCancelAllByPairRequest) returns (BatchCancelAllResponse);
rpc BatchCreateOrders(BatchCreateOrdersRequest) returns (BatchCreateOrdersResponse);
rpc BatchReplaceOrders(BatchReplaceOrdersRequest) returns (BatchReplaceOrdersResponse);
rpc ReplaceOrder(ReplaceOrderRequest) returns (ReplaceOrderResponse);
rpc GetOrder(GetOrderRequest) returns (GetOrderResponse);
rpc ListOrders(ListOrdersRequest) returns (ListOrdersResponse);
rpc CancelConditionalOrder(CancelConditionalOrderRequest) returns (CancelConditionalOrderResponse);
rpc ListConditionalOrders(ListConditionalOrdersRequest) returns (ListConditionalOrdersResponse);
rpc GetConditionalOrder(GetConditionalOrderRequest) returns (GetConditionalOrderResponse);
rpc CreateTwapOrder(CreateTwapOrderRequest) returns (CreateTwapOrderResponse);
rpc CancelTwapOrder(CancelTwapOrderRequest) returns (CancelTwapOrderResponse);
rpc GetTwapOrder(GetTwapOrderRequest) returns (TwapOrder);
rpc ListTwapOrders(ListTwapOrdersRequest) returns (ListTwapOrdersResponse);
rpc GetTwapOrderTca(GetTwapOrderTcaRequest) returns (GetTwapOrderTcaResponse);
}
```
## Notes
* Order creation, cancellation, replacement, and listing require authentication.
* Batch RPCs are the preferred path for multi-order workflows.
* Attach TP/SL through order placement or `PositionsService.AttachPositionTpSl`.
* `GetConditionalOrder` fetches a single TP/SL order by UUID — the same IDs returned as `take_profit_order_id` and `stop_loss_order_id` in `CreateOrderResponse`.
* A trigger and the order it fires into are linked in both directions, and both reads are owner-scoped. `GetConditionalOrder` and `ListConditionalOrders` carry `triggered_order` — the close's current status, filled quantity, average fill price, taker fees, fill time and terminal reason, read from that order at request time — present only once `state` is TRIGGERED and the close can be read, never on a FAILED conditional, and never on the `conditional_orders` WebSocket frames. `GetOrder` / `ListOrders` carry the reverse pointer `conditional_order_id` on a close the engine placed for a trigger; it is absent on client-placed orders, on TWAP children (they carry `parent_order_id`), and on closes older than the field, which are not backfilled.
* `ListOrders` returns book orders only (`LIMIT` and `MARKET`): TWAP parents and conditional (TP/SL) orders are not rows of it under any `status` or `trading_mode` filter and are not counted in `total`; list them with `ListTwapOrders` and `ListConditionalOrders`. The close a trigger fired into is a book order and is listed as its own row carrying `conditional_order_id`.
* `ReplaceOrder` and `BatchReplaceOrders` refuse to replace the LIMIT leg a TP/SL fired into — an order carrying `conditional_order_id` — the way a passive TWAP child is refused: a replacement rests under a new order id the conditional does not point at, which would sever the link both ways. The refusal names the conditional order and the original leg stays resting; in a batch the refused item's original is never cancelled. Cancelling a triggered leg is unchanged.
* TWAP parent orders execute a large order as scheduled market child orders across a time window. `CreateTwapOrder`, `CancelTwapOrder`, `GetTwapOrder`, and `ListTwapOrders` manage the parent lifecycle; `GetTwapOrder` returns the `TwapOrder` read model directly. Delegated-agent sessions are rejected for all TWAP RPCs. See [TWAP Orders](/trading-mechanics/twap-orders) for the execution model.
* `GetTwapOrderTca` returns the post-execution transaction cost analysis report for a **terminal** TWAP parent (`COMPLETED` or `CANCELLED`). A non-terminal parent is `INVALID_ARGUMENT`; another user's parent is `NOT_FOUND`, indistinguishable from an unknown ID. The six analytics fields are optional and absent when they cannot be computed honestly.
# TraderCodeService
Source: https://docs.0xmonaco.com/grpc/protos/pitpass
TraderCodes referral codes and rewards
Package: `monaco.api.pitpass`
Source: `protos/api/pitpass.proto`
Use `TraderCodeService` to retrieve and validate TraderCodes, read your referral position, downline, and referral tree, read a user's rewards balance, and transfer earned rewards into a trading balance.
```protobuf theme={null}
service TraderCodeService {
rpc GetMyTraderCode(GetMyTraderCodeRequest) returns (TraderCodeResponse);
rpc GetTraderCodeInfo(GetTraderCodeInfoRequest) returns (GetTraderCodeInfoResponse);
rpc GetRewardsBalance(GetRewardsBalanceRequest) returns (GetRewardsBalanceResponse);
rpc TransferRewards(TransferRewardsRequest) returns (TransferRewardsResponse);
rpc GetMyReferralPosition(GetMyReferralPositionRequest) returns (GetMyReferralPositionResponse);
rpc GetMyReferralDownline(GetMyReferralDownlineRequest) returns (GetMyReferralDownlineResponse);
rpc GetMyReferralTree(GetMyReferralTreeRequest) returns (GetMyReferralTreeResponse);
}
```
## Notes
* `GetMyTraderCode` requires authentication. It is idempotent and never returns `NOT_FOUND` — the code row is created on demand for the authenticated wallet.
* `GetTraderCodeInfo` is public (no auth). It accepts a bare wallet address or the `Monaco - ` display form. Returns `NOT_FOUND` when the code does not resolve to a known wallet.
* `GetRewardsBalance` returns an empty `balances` list when no rewards have been earned yet. `available` values are in raw atomic units.
* `TransferRewards` moves the specified amount from the caller's rewards bucket into the calling application's trading balance. The transfer is ledger-only — no on-chain transaction. Returns `FAILED_PRECONDITION` when the rewards balance is insufficient. Each call spends the per-user movement budget withdrawals and collateral transfers share, whatever the balance; `RESOURCE_EXHAUSTED` when that budget is enforced and spent.
* Monaco configures how many referral levels TraderCodes rewards (1 to 10) and a rate per level; rates may change over time and apply to future fills only. The `rate_applied` on each reward row is captured at fill time. Reward `level` is 1 for the direct referrer, 2 for the referrer's referrer, and so on (at most 10).
* `GetMyReferralPosition`, `GetMyReferralDownline`, and `GetMyReferralTree` are authenticated and **self-scoped**: identity comes only from the session, never a request id, so a caller can only ever read their own referral chain. All three map every other user to their public wallet address (with a `display` custom-handle-or-wallet variant), never an internal id.
* `GetMyReferralPosition` returns your `upline` — the chain above you, nearest first: L1 (your direct referrer), L2, and so on, with at most as many entries as TraderCodes rewards levels (at most 10) — and the L1 wallet again as `referrer` (both empty when you were not referred), your all-time `total_earned`, and your most recent reward rows (up to the 100 newest; `total_earned` stays the complete aggregate).
* `GetMyReferralDownline` returns one page of `earnings_by_source` across every reward level, ordered by each source account's total, largest first (a wallet's per-application rows are merged within a page at the first row's position); your L1 direct-referee roster (including referees who have not traded yet, shown with `0` earned; showing up to the 500 most recent effective referees (after per-wallet dedupe), drawn from a bounded scan so a very large history may yield an incomplete roster, while the earnings views stay complete); and a per-level `summary`. Page with `page_size` (1–500, default 100) and `page_token` (the opaque `next_page_token` from the previous response; `next_page_token` is empty when no rows remain). A request without paging fields returns only the first page, not every row — follow `next_page_token` until it is empty. Rows of a wallet that traded through several applications are merged within a page, but that wallet can appear again on a later page. `summary` covers every level and source regardless of the page: `summary.by_level` lists `{ level, earned }` (raw atomic units) in ascending level order, one entry per level with earnings, including levels deeper than TraderCodes rewards today. `summary.level1_earned`, `level2_earned`, and `level3_earned` are deprecated (levels 1–3 only); use `by_level`.
* `GetMyReferralTree` returns one level of your downline per call: with no `parent`, your L1 referees; with `parent` set to one of your referees above the deepest rewarded level, that wallet's referees. Each `ReferralTreeNode` carries `level`, `parent`, `referred_at`, `referee_count` (expandable when above 0; always 0 at the deepest rewarded level), and `total_earned` from that wallet's own trades. Pages hold up to 200 nodes (`page_size`, default 50) with a `next_page_token` that is only valid for the same `parent`. Each wallet appears once, under the referrer that earns from it. `level` ranges 1–10 and the response's `parent_level` 0–9. A `parent` that is not you or one of your referees above the deepest rewarded level returns `NOT_FOUND`; a malformed `parent`, `page_size`, or `page_token` returns `INVALID_ARGUMENT`.
* The referral reads share the standard authenticated-read error set: `UNAUTHENTICATED` when the session is missing or invalid, `RESOURCE_EXHAUSTED` when the account's read budget is exhausted (retry after the interval in the `RetryInfo` detail), and `INTERNAL` on a server error.
# PositionsService
Source: https://docs.0xmonaco.com/grpc/protos/positions
Margin positions, close flows, risk, margin adjustments, history, and TP/SL
Package: `monaco.api.positions`
Source: `protos/api/positions.proto`
Use `PositionsService` for isolated-margin position management.
```protobuf theme={null}
service PositionsService {
rpc ListPositions(ListPositionsRequest) returns (ListPositionsResponse);
rpc GetPosition(GetPositionRequest) returns (GetPositionResponse);
rpc ClosePosition(ClosePositionRequest) returns (ClosePositionResponse);
rpc GetPositionRisk(GetPositionRiskRequest) returns (GetPositionRiskResponse);
rpc ListPositionHistory(ListPositionHistoryRequest) returns (ListPositionHistoryResponse);
rpc AttachPositionTpSl(AttachPositionTpSlRequest) returns (AttachPositionTpSlResponse);
}
```
## Notes
* Position RPCs are authenticated.
* TP/SL can be attached to existing positions through `AttachPositionTpSl`.
* `ListPositions`, `GetPosition` and `GetPositionRisk` read each market's index price from the matching engine and return `UNAVAILABLE` when that read fails transiently (retry); any other failure of that read is `INTERNAL`.
* `ClosePosition` draws one item from the per-account risk-reduction order budget it shares with cancels; `BatchCloseAll` draws one item per open position it closes (an empty call draws one): the first 100 are charged before anything is sent and over budget the whole call is refused, while positions past 100 that the budget does not cover are not sent and come back in `results` with `RATE_LIMIT_EXCEEDED`. Both return `RESOURCE_EXHAUSTED` with a `google.rpc.RetryInfo` detail when the budget is enforced and spent, before anything reaches the matching engine. See [Rate Limits](/developers/rate-limits).
* `Position.cum_fees`, when present, is the whole-life fee total on open and terminal rows; it is absent while fee history is unavailable or incomplete — before the position's first PnL sample, after retention, or on a terminal row until the final close is recorded. On a terminal row, `exit_price`, `funding_paid`, `fees_paid` (closing-side only), `net_realized_pnl` and `realized_roe` come from the reducing executions when they reconcile with `realized_pnl`, and `size` / `isolated_margin` carry the lifetime closed quantity and allocated initial margin.
* `ListPositions` and `ListPositionHistory` are cursor-paginated: send an empty `page_token` to start, then each response's `next_page_token` until it comes back empty; `page_size` is 1 to 1000. There is no `page` field: a stub generated from an older proto that still sets it is not rejected (protobuf discards the unknown field) and reads the first page on every call, so regenerate stubs and walk the token.
* `Position.opened_at` (RFC 3339) is the fill that took the position's exposure from zero to non-zero; it never changes for the position id and is absent when unknown.
# PulseService
Source: https://docs.0xmonaco.com/grpc/protos/pulse
Cached market news and sentiment feed
Package: `monaco.api.pulse`
Source: `protos/api/pulse.proto`
Use `PulseService` for the backend-cached market pulse news and sentiment feed. It is public — no auth — and serves the same snapshot as `GET /api/v1/pulse`.
```protobuf theme={null}
service PulseService {
rpc GetPulse(GetPulseRequest) returns (GetPulseResponse);
}
```
## Notes
* `GetPulse` is public and takes an empty `GetPulseRequest`.
* `GetPulseResponse` carries `generated_at` (ISO 8601 UTC), `watchlist`, and `items` — each `PulseItem` has `asset` (ticker or `"MARKET"`), `sentiment` (`"Bullish"`/`"Neutral"`/`"Bearish"`), `summary`, `updated_at`, and `sources` (each `PulseSource` has `url`, `author`, `source`, `created_at`, `relevance` 1–5).
* The snapshot refreshes hourly server-side; the RPC returns `UNAVAILABLE` until the cache warms after a deploy.
# SweeperService
Source: https://docs.0xmonaco.com/grpc/protos/sweeper
Registered deposit-address monitoring and automatic sweeping
Package: `monaco.api.sweeper`
Source: `protos/api/sweeper.proto`
Use `SweeperService` to register a deposit address so the watched assets sent there are swept into the rollup automatically once each asset's minimum is reached, and to read the chains and assets it watches. Only the assets `GetChains` lists are indexed — the transfer indexer filters logs to those token addresses — so an unlisted ERC20 sent to the address is not swept.
```protobuf theme={null}
service SweeperService {
rpc Register(RegisterRequest) returns (RegisterResponse);
rpc GetChains(GetChainsRequest) returns (GetChainsResponse);
}
```
## Notes
* `Register` requires authentication, and the request carries no identity at all: the application is the one the session was established under (`login` takes its client id) and the credited address is the session's own. A caller can therefore only register their own address under their own application. An unauthenticated call returns `UNAUTHENTICATED`.
* Every `(application, user, deposit target)` triple has a deterministic deposit address; registering the triple makes the backend monitor that address and forward deposits of the watched assets into the rollup, crediting the user under the application. `GetChains` is the authoritative list of what is watched; a token outside it is not indexed and not swept.
* `deposit_target` is optional: `SPOT` (the default when omitted or empty) credits the spot wallet, `MARGIN` asks for the parent margin account's collateral. It is part of the address derivation, so `SPOT` and `MARGIN` yield **two different deposit addresses** for the same application and user, each carrying its own request. A `SPOT` address is never credited to margin.
* `MARGIN` is a routing request rather than a guarantee: a deposit the matching engine cannot route to collateral — an unsupported collateral asset, or a margin account that fails validation — is credited to the spot wallet instead, so funds are never stranded.
* The response returns immediately with the derived `sweeper_address` and a `status` of `registered` (added by this call) or `already_registered` (known before). A one-time initial balance check — which sweeps whatever is already sitting on the address at or above that asset's minimum, the same floor every later sweep applies — runs in the background and is reported by `initial_sweep_check`.
* Idempotent and safe to retry within the current forwarder-based derivation: re-registering an existing triple returns `already_registered` with the same derived address. A completed initial check is never repeated; if `initial_sweep_check` is still `false`, registration schedules it again, while an already-running check is deduplicated.
* **Pre-v1.0.53 registrations need an operator migration.** Calling `Register` for one returns and monitors the new forwarder-derived address in the running resolver, but does not replace that row's stored address; after a resolver restart, warmup reloads only the old value. Pause deposits for that registration and do not display or fund the new address until Monaco confirms its stored row was migrated. Re-registration alone is not restart-durable.
* A `deposit_target` that is neither `SPOT` nor `MARGIN` returns `INVALID_ARGUMENT`. An unrecognized target is rejected rather than defaulting to `SPOT`, which would answer with an address pointed at a ledger the caller did not ask for.
* `PERMISSION_DENIED` means the application the session was established under is no longer active.
* `RESOURCE_EXHAUSTED` means the service-wide admission limit on *new* registrations was hit. They create durable state, so they are bounded across all callers rather than per caller; re-registering an existing address is never limited, and a refused call stores nothing, so it is safe to retry. The status carries a `google.rpc.RetryInfo` detail with the wait — the same interval REST returns as `retryAfter` — so honor that rather than guessing.
* `INTERNAL` is a generic server-side failure. It can happen before the sweeper is contacted at all — a failed application lookup, for instance — so unlike `UNAVAILABLE` it says nothing about the resolver.
* `GetChains` is public: it carries no auth interceptor requirement and takes no request fields, because a depositor needs the supported set before they have a session. It returns one `SweeperChain` per watched chain — the EVM `chain_id`, a short `name`, a `hub` flag marking the settlement chain that deposits on every other listed chain are bridged into, and the `SweeperChainAsset` entries swept there (`symbol`, `token_address`, `decimals`, and `min_sweep_raw`, the minimum sweepable balance in raw base units). 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. 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 treating a cached set as permanent.
* Every registered deposit address is watched on **every** listed chain and is identical on all of them, so `Register` takes no chain and one registration covers the whole set. `GetChains` is for showing a depositor where they may send, not for choosing where to register.
* `GetChains` is all-or-nothing: rather than publish a partial listing, it returns `UNAVAILABLE` whenever any configured chain cannot be advertised — the sweeper is not configured at all, a configured chain is disabled by its own configuration, a chain's id is not yet verified against its RPC (an unreachable RPC included; until it verifies, its indexer is still seeding and nothing watches that chain), a chain's RPC reports an id outside the JSON-safe range the listing serves, or the resolver could not answer. All of them are safe to retry. `INTERNAL` is a generic server-side failure.
* `UNAVAILABLE` means the sweeper could not complete the registration. Several unrelated conditions share it — missing configuration, an unreachable chain or factory, a store that would not write or read back, a reply that never arrived — so treat it as an **unknown** outcome rather than a negative one: the registration is stored before the reply is composed, and the status alone never tells you whether that happened. Retry either way — registration is idempotent, and the retry answers `registered` or `already_registered` according to whether the first attempt had landed. Both are success and both carry the same address, so accept either.
# TradesService
Source: https://docs.0xmonaco.com/grpc/protos/trades
Public recent trades and authenticated user trade history
Package: `monaco.api.trades`
Source: `protos/api/trades.proto`
Use `TradesService` for recent trade history, single-trade lookup, and authenticated user trade history.
```protobuf theme={null}
service TradesService {
rpc GetTrades(GetTradesRequest) returns (GetTradesResponse);
rpc GetTradesRPC(GetTradesRPCRequest) returns (GetTradesResponse);
rpc GetTradeById(GetTradeByIdRequest) returns (GetTradeByIdResponse);
rpc ListUserTrades(ListUserTradesRequest) returns (ListUserTradesResponse);
}
```
## Notes
* Recent public trades do not require authentication.
* `GetTrades` is cursor-paginated, newest first: send an empty `page_token` to start, then each response's `next_page_token` until it comes back empty; an empty token is definitive. `page_size` defaults to `25` and must be 1–1000. The walk seeks on `(executed_at, id)`, so a page never repeats or skips a trade when new fills land mid-walk. The token is pinned to its `trading_pair_id`, so a token reused for another pair, or one minted by `ListUserTrades`, is rejected. There is no `page` field: code that still sets it fails to compile against a regenerated stub, and a stub generated from an older proto has the unknown field discarded and reads the first page on every call. The walk covers the recent trades window, not a pair's full archived history.
* `ListUserTrades` is authenticated and gRPC-only.
* `ListUserTrades` returns `UserTrade.fee` as the total fee paid (`monaco_fee` + `builder_fee`); `builder_fee` is `"0"` for a maker. All three are integer strings in raw quote units, scaled by `10^quoteDecimals` (so `4178` is `0.004178` at six decimals); this message carries no separate normalized fee fields.
* `ListUserTrades` accepts an optional `order_id` UUID that returns only trades where that order placed the caller's own side of the fill, across hot and archived history. Adding or changing it invalidates an existing `page_token` — restart the cursor walk with an empty token.
# WhitelistService
Source: https://docs.0xmonaco.com/grpc/protos/whitelist
Retired public whitelist application endpoint
Package: `monaco.api.whitelist`
Source: `protos/api/whitelist.proto`
Public whitelist applications are closed. `SubmitWhitelist` remains available for client compatibility and returns `PERMISSION_DENIED` without changing applicant data. Existing applications and administrative approvals remain intact.
```protobuf theme={null}
service WhitelistService {
rpc SubmitWhitelist(SubmitWhitelistRequest) returns (SubmitWhitelistResponse);
}
```
## Notes
* `SubmitWhitelistRequest` requires `wallet_address` and `email`.
* Optional fields include Twitter/X and Telegram usernames.
# WithdrawalsService
Source: https://docs.0xmonaco.com/grpc/protos/withdrawals
Withdrawal initiation and executable calldata lookup
Package: `monaco.api.withdrawals`
Source: `protos/api/withdrawals.proto`
Use `WithdrawalsService` to allocate withdrawals, list the ones still awaiting confirmation, and retrieve ABI-encoded `executeWithdrawal(...)` vault calldata for on-chain submission once the merkle proof is available.
```protobuf theme={null}
service WithdrawalsService {
rpc InitiateWithdrawal(InitiateWithdrawalRequest) returns (Withdrawal);
rpc GetWithdrawal(GetWithdrawalRequest) returns (Withdrawal);
rpc ListPendingWithdrawals(ListPendingWithdrawalsRequest) returns (ListPendingWithdrawalsResponse);
}
```
## Notes
* Initiating a withdrawal requires authentication and withdraw permission.
* `InitiateWithdrawal` returns an empty `calldata` field until the withdrawal root is confirmed on-chain.
* `GetWithdrawal` can re-fetch the merkle-proof-backed `executeWithdrawal(...)` calldata once it is ready.
* `ListPendingWithdrawals` is authenticated and returns only the caller's own withdrawals that are still awaiting on-chain root confirmation, newest first. Each entry carries `withdrawal_index`, `asset_id`, `asset_symbol`, the raw `amount`, `destination`, `status` (always `pending`), and `created_at`, but no calldata: fetch that with `GetWithdrawal` once the withdrawal is confirmed.
* `ListPendingWithdrawals` is cursor-paginated: send an empty `page_token` to start, then each response's `next_page_token` until it comes back empty; an empty token is definitive. `page_size` defaults to `20` and must be 1–1000, and a larger value is rejected rather than clamped. `total` and `total_pages` are exact. There is no `page` field: a stub generated from an older proto that still sets it has the unknown field discarded and reads the first page on every call, so regenerate it.
* `ListPendingWithdrawals` returns `INVALID_ARGUMENT` for an invalid page size or token, `UNAUTHENTICATED` without a session, `RESOURCE_EXHAUSTED` when the account's read budget is exhausted and enforced (retry after `RetryInfo`), and `INTERNAL` on a backend failure.
# Introduction
Source: https://docs.0xmonaco.com/introduction
Spot and perps trading infrastructure for traders, builders, or AI agents.
Monaco
Spot and perps markets for
APIs and SDKs for high-performance access to financial markets.
## What is Monaco?
Monaco is a high-performance trading platform for [spot](/trading-mechanics/spot) and [perpetual](/trading-mechanics/perps) markets, with prediction markets incoming, built using a simple and robust API stack. Trading is powered by Monaco's low-latency institutional-grade order book, where SDKs and APIs provide everything needed for anyone to access trading infrastructure: builders, traders, and agents.
Prefer to chat with an agent instead of searching the docs?
AI & Agents
→
## Why Monaco?
For builders looking to bootstrap liquidity without the overhead of designing, maintaining, and improving a sophisticated risk and matching engine, Monaco is a full-stack solution. For traders looking to access global markets with highly competitive fees, rebates, and capital efficiency, Monaco offers a sophisticated trading experience.
* **Real Revenue to Users:** maker rebates for both retail and institutional participants at all volume levels, with BuilderCodes for any integrated frontend, and TraderCodes for multi-layer referrals.
* **Mature Market Design:** price-time priority alongside prioritized cancels, low-latency matching, and simplified funding rates in a shared liquidity layer.
* **Robust Risk Management:** margin accounts can start with cross-margin, but will support correlation and scenario-based portfolio margin to allow netting benefits for offsetting correlated positions.
* **Open Integration:** REST, WebSocket, gRPC, TypeScript, React, Rust, generated clients, and MCP tooling allow traders, application builders, market makers, and AI agents to use unified market infrastructure.
Revenue-sharing for maker liquidity, building, and referrals
Learn how to earn with Monaco
→
## Trade on Monaco
For traders who prefer a terminal, you can access Monaco through your preferred platform, or access it programmatically using the existing SDKs and API stack.
* [**Mach 1**](https://app.m1.markets/en)
* [**Monte Carlo**](https://www.montecarlo.trade/)
Want to build a Monaco-powered app and have it listed here? Reach out to [team@0xmonaco.com](mailto:team@0xmonaco.com) for an introduction.
## Building on Monaco
Anyone can build tailored trading experiences, analytics dashboards, market-maker systems, internal tooling, or AI agent workflows leveraging Monaco's unified order book. Monaco provides the core infrastructure, so builders can focus on the product experience.
BuilderCodes allow applications to earn protocol revenue on the volume they route directly to the order book. Visit the [builder quickstart](/quickstart) to get started.
# Markets
Source: https://docs.0xmonaco.com/markets
View the supported spot and perps markets available on Monaco, including leverage and product support.
## Available Markets
## Propose New Markets and Earn
Monaco is designed to let proposers bring new markets to shared liquidity without paying a listing fee. A proposer supplies the required minimum liquidity for a market and can receive a 50% revenue share from activity generated in that market while it remains listed and active.
# Quickstart
Source: https://docs.0xmonaco.com/quickstart
All interactions with Monaco happen through REST, WebSocket, or gRPC APIs. Connect directly, or use an SDK to manage the details for you.
## API URLs
```text REST theme={null}
https://staging.apimonaco.xyz/api
```
```text WebSocket theme={null}
wss://ws-staging.apimonaco.xyz/ws
```
```text gRPC theme={null}
https://staging.apimonaco.xyz
```
## Build Surface Map
Each Build page starts with a surface map: use it to choose the REST route family, TypeScript SDK namespace, live WebSocket channel, or gRPC service for the action on that page.
| Family | Build page | Primary SDK namespace | REST / stream family |
| - | - | - | - |
| Wallets and auth | [Wallets & Auth](/developers/accounts/wallets) | `sdk.login()`, `sdk.auth` | `/auth`, WebSocket `Authenticate` |
| Balances | [Account Balances](/developers/account-balances) | `sdk.profile` | `/accounts`, `balances`, `movements` |
| Deposits and withdrawals | [Vault Deposits & Withdrawals](/developers/vault) | `sdk.vault`, `sdk.withdrawals` | `/withdrawals` plus vault contract actions |
| Perps collateral | [Perps Collateral](/developers/perps-collateral) | `sdk.marginAccounts` | `/margin` |
| Markets | [Markets](/developers/markets) | `sdk.market` | `/market` |
| Orderbooks and trades | [Orderbooks & Trades](/developers/orderbooks-trades) | `sdk.orderbook`, `sdk.trades` | `/orderbook`, `/trades`, public streams |
| Orders | [Order Management](/developers/trade) | `sdk.trading` | `/orders`, order streams |
| Positions | [Positions](/developers/positions) | `sdk.positions` | `/positions`, raw position streams |
| Delegated agents | [Delegated Agents](/developers/delegated-agents) | `sdk.delegatedAgents` | `/delegated-agents` |
| Test assets | [Testnet Faucet](/developers/faucet) | `sdk.faucet` | `/faucet` |
## SDKs
Official SDKs handle Monaco auth, request signing, request formatting, and trading workflows. Generated clients expose typed REST or gRPC clients for other languages.
### TypeScript
} href="/sdk/typescript/intro">
Build authenticated clients, sign requests, place and cancel orders, query markets and profiles, manage vault flows, and subscribe to live feeds.
REST
WebSocket
### React
} href="/sdk/react/intro">
Provider and hooks for wallet login, live orderbooks and trades, user balances, positions, orders, fees, and vault actions in React apps.
REST
WebSocket
### Rust
} href="/sdk/rust-and-bsr">
Backend clients for authenticated REST and gRPC integrations, including request signing, order management, account data, and market data.
REST
gRPC
### Other languages
Generate typed REST or gRPC clients from Monaco's published protobuf files for Go, Python, Java, C++, and other backend languages. Start with [Python, Go, C++ & More](/sdk/python-quickstart) or the [Buf registry](https://buf.build/monaco-sdk/api/tree).
## Authentication
Public market data can be read without a user session. Trading, account, deposit, withdrawal, and other user-specific routes require an authenticated wallet session. SDKs handle session creation, request signing, and session refresh. Direct integrations sign each request with a session key: over REST follow the [REST authentication flow](/api-reference#authentication-flow), and over gRPC follow [authenticated gRPC](/reference/grpc#authenticate-requests) (metadata names, canonical signing string, and a runnable signed example). See [Wallets & Auth](/developers/accounts/wallets) for Sei network, gas, wallet setup, and asset IDs.
Private alpha requirements
Authenticated requests require a Monaco client ID. Email [team@0xmonaco.com](mailto:team@0xmonaco.com) to get one.
## APIs
Applications require different connection patterns. REST is the most compatible request/response interface, WebSockets are for live market and account events, and gRPC is the typed low-latency path for backend services. Choose based on your infrastructure, tooling, and latency requirements.
### REST API
} href="/api-reference">
Standard HTTP/JSON access to Monaco routes for market data, account state, orders, deposits, withdrawals, and admin workflows. Use it when you want broad tooling support, predictable request/response calls, or OpenAPI-based clients.
Most compatible and standard
### WebSocket API
} href="/reference/websockets">
Configurable live subscriptions for Monaco events: all trades, orderbook updates, candles, and authenticated streams for your own orders, balances, positions, and liquidation risk. Use it when your UI or service needs data pushed as markets move.
Best for listening to data
### gRPC API
} href="/reference/grpc">
Strongly typed HTTP/2 services for backend systems that need fast order entry, account queries, market data, and generated clients from Monaco protobuf contracts. Use it when latency and typed service contracts matter more than generic HTTP tooling.
Fastest for trading quickly
# gRPC API
Source: https://docs.0xmonaco.com/reference/grpc
Endpoint, protobuf module, and available services
Use gRPC when you want generated clients, strongly typed request/response contracts, or backend-to-backend integration. Monaco publishes the protobuf module to BSR at [`buf.build/monaco-sdk/api`](https://buf.build/monaco-sdk/api).
## Endpoint
```text theme={null}
https://staging.apimonaco.xyz
```
## Protobuf Module
```text theme={null}
buf.build/monaco-sdk/api:main
```
See the [gRPC protobuf reference](/grpc/protos) for the curated protobuf file, package, service, and message reference.
## Services
Wallet authentication, session refresh, and session revocation.
```protobuf theme={null}
rpc Challenge(ChallengeRequest) returns (ChallengeResponse);
rpc Verify(VerifyRequest) returns (VerifyResponse);
rpc Refresh(RefreshRequest) returns (RefreshResponse);
rpc Revoke(RevokeRequest) returns (RevokeResponse);
```
Order entry, cancellation, replacement, batch operations, order lookup, and conditional TP/SL order management.
```protobuf theme={null}
rpc CreateOrder(CreateOrderRequest) returns (CreateOrderResponse);
rpc CancelOrder(CancelOrderRequest) returns (CancelOrderResponse);
rpc BatchCancelOrders(BatchCancelOrdersRequest) returns (BatchCancelOrdersResponse);
rpc BatchCancelAll(BatchCancelAllRequest) returns (BatchCancelAllResponse);
rpc BatchCancelAllByPair(BatchCancelAllByPairRequest) returns (BatchCancelAllResponse);
rpc BatchCreateOrders(BatchCreateOrdersRequest) returns (BatchCreateOrdersResponse);
rpc BatchReplaceOrders(BatchReplaceOrdersRequest) returns (BatchReplaceOrdersResponse);
rpc ReplaceOrder(ReplaceOrderRequest) returns (ReplaceOrderResponse);
rpc GetOrder(GetOrderRequest) returns (GetOrderResponse);
rpc ListOrders(ListOrdersRequest) returns (ListOrdersResponse);
rpc CancelConditionalOrder(CancelConditionalOrderRequest) returns (CancelConditionalOrderResponse);
rpc ListConditionalOrders(ListConditionalOrdersRequest) returns (ListConditionalOrdersResponse);
```
Profile, balances, ledger movements, funding payments, sub-accounts, user trades, and portfolio analytics.
```protobuf theme={null}
rpc GetProfile(GetProfileRequest) returns (GetProfileResponse);
rpc GetBalances(GetBalancesRequest) returns (GetBalancesResponse);
rpc GetBalanceByAsset(GetBalanceByAssetRequest) returns (GetBalanceByAssetResponse);
rpc GetMovements(GetMovementsRequest) returns (GetMovementsResponse);
rpc ListFundingPayments(ListFundingPaymentsRequest) returns (ListFundingPaymentsResponse);
rpc ListSubAccounts(ListSubAccountsRequest) returns (ListSubAccountsResponse);
rpc CreateLimit(CreateLimitRequest) returns (CreateLimitResponse);
rpc UpdateLimit(UpdateLimitRequest) returns (UpdateLimitResponse);
rpc GetLimits(GetLimitsRequest) returns (GetLimitsResponse);
rpc GetUserTrades(GetUserTradesRequest) returns (GetUserTradesResponse);
rpc GetPortfolioStats(GetPortfolioStatsRequest) returns (GetPortfolioStatsResponse);
rpc GetPortfolioChart(GetPortfolioChartRequest) returns (GetPortfolioChartResponse);
rpc DeleteLimit(DeleteLimitRequest) returns (DeleteLimitResponse);
```
Public market data, candles, screener data, perp market config, funding, open interest, mark price, and index price.
```protobuf theme={null}
rpc ListTradingPairs(ListTradingPairsRequest) returns (ListTradingPairsResponse);
rpc GetTradingPair(GetTradingPairRequest) returns (GetTradingPairResponse);
rpc GetCandles(GetCandlesRequest) returns (GetCandlesResponse);
rpc GetMarketMetadata(GetMarketMetadataRequest) returns (GetMarketMetadataResponse);
rpc GetScreener(GetScreenerRequest) returns (GetScreenerResponse);
rpc GetOpenInterest(GetOpenInterestRequest) returns (GetOpenInterestResponse);
rpc GetFundingState(GetFundingStateRequest) returns (GetFundingStateResponse);
rpc ListFundingHistory(ListFundingHistoryRequest) returns (ListFundingHistoryResponse);
rpc GetPerpMarketConfig(GetPerpMarketConfigRequest) returns (GetPerpMarketConfigResponse);
rpc GetPerpMarketSummary(GetPerpMarketSummaryRequest) returns (GetPerpMarketSummaryResponse);
rpc GetMarkPrice(GetMarkPriceRequest) returns (GetMarkPriceResponse);
rpc GetIndexPrice(GetIndexPriceRequest) returns (GetIndexPriceResponse);
```
Public orderbook snapshots. `GetOrderbookRPC` is a gRPC-only wrapper over the same response type.
```protobuf theme={null}
rpc GetOrderbook(GetOrderbookRequest) returns (GetOrderbookResponse);
rpc GetOrderbookRPC(GetOrderbookRPCRequest) returns (GetOrderbookResponse);
```
Public trade history, single-trade lookup, and authenticated user trade history.
```protobuf theme={null}
rpc GetTrades(GetTradesRequest) returns (GetTradesResponse);
rpc GetTradesRPC(GetTradesRPCRequest) returns (GetTradesResponse);
rpc GetTradeById(GetTradeByIdRequest) returns (GetTradeByIdResponse);
rpc ListUserTrades(ListUserTradesRequest) returns (ListUserTradesResponse);
```
Isolated-margin account summaries, collateral availability, collateral transfers, movements, and order-risk simulation.
```protobuf theme={null}
rpc GetMarginAccountSummary(GetMarginAccountSummaryRequest) returns (GetMarginAccountSummaryResponse);
rpc GetParentMarginAccountSummary(GetParentMarginAccountSummaryRequest) returns (GetMarginAccountSummaryResponse);
rpc ListMarginAccounts(ListMarginAccountsRequest) returns (ListMarginAccountsResponse);
rpc GetAvailableCollateral(GetAvailableCollateralRequest) returns (GetAvailableCollateralResponse);
rpc TransferCollateralToMarginAccount(TransferCollateralToMarginAccountRequest) returns (TransferCollateralToMarginAccountResponse);
rpc TransferCollateralFromMarginAccount(TransferCollateralFromMarginAccountRequest) returns (TransferCollateralFromMarginAccountResponse);
rpc TransferCollateralToParentMarginAccount(TransferCollateralToParentMarginAccountRequest) returns (TransferCollateralToMarginAccountResponse);
rpc TransferCollateralFromParentMarginAccount(TransferCollateralFromParentMarginAccountRequest) returns (TransferCollateralFromMarginAccountResponse);
rpc GetMarginAccountMovements(GetMarginAccountMovementsRequest) returns (GetMarginAccountMovementsResponse);
rpc GetParentMarginAccountMovements(GetParentMarginAccountMovementsRequest) returns (GetMarginAccountMovementsResponse);
rpc SimulateOrderRisk(SimulateOrderRiskRequest) returns (SimulateOrderRiskResponse);
rpc SimulateParentMarginOrderRisk(SimulateParentMarginOrderRiskRequest) returns (SimulateOrderRiskResponse);
rpc TransferCollateralToRiskBucket(TransferCollateralToRiskBucketRequest) returns (TransferCollateralToMarginAccountResponse);
rpc SimulateRiskBucketOrderRisk(SimulateRiskBucketOrderRiskRequest) returns (SimulateOrderRiskResponse);
rpc CreateSubAccount(CreateSubAccountRequest) returns (CreateSubAccountResponse);
rpc CloseSubAccount(CloseSubAccountRequest) returns (CloseSubAccountResponse);
rpc GetMarginAccountPnl(GetMarginAccountPnlRequest) returns (GetMarginAccountPnlResponse);
rpc TransferCollateral(TransferCollateralRequest) returns (TransferCollateralResponse);
rpc TransferCollateralToSubAccount(TransferCollateralToSubAccountRequest) returns (TransferCollateralResponse);
rpc TransferCollateralFromSubAccount(TransferCollateralFromSubAccountRequest) returns (TransferCollateralResponse);
```
Margin positions, close flows, risk checks, margin adjustments, position history, and TP/SL attachment.
```protobuf theme={null}
rpc ListPositions(ListPositionsRequest) returns (ListPositionsResponse);
rpc GetPosition(GetPositionRequest) returns (GetPositionResponse);
rpc ClosePosition(ClosePositionRequest) returns (ClosePositionResponse);
rpc GetPositionRisk(GetPositionRiskRequest) returns (GetPositionRiskResponse);
rpc ListPositionHistory(ListPositionHistoryRequest) returns (ListPositionHistoryResponse);
rpc AttachPositionTpSl(AttachPositionTpSlRequest) returns (AttachPositionTpSlResponse);
```
Application configuration (session-authenticated) and public, `client_id`-selected users, balances, orders, movements, and stats.
```protobuf theme={null}
rpc GetConfig(GetConfigRequest) returns (GetConfigResponse);
rpc ListAppOrders(ListAppOrdersRequest) returns (ListAppOrdersResponse);
rpc ListAppUsers(ListAppUsersRequest) returns (ListAppUsersResponse);
rpc ListAppMovements(ListAppMovementsRequest) returns (ListAppMovementsResponse);
rpc GetAppStats(GetAppStatsRequest) returns (GetAppStatsResponse);
rpc ListAppBalances(ListAppBalancesRequest) returns (ListAppBalancesResponse);
```
Delegated-agent registration, owner lookup, revocation, and delegated-session creation.
```protobuf theme={null}
rpc UpsertDelegatedAgent(UpsertDelegatedAgentRequest) returns (DelegatedAgent);
rpc ListDelegatedAgents(ListDelegatedAgentsRequest) returns (ListDelegatedAgentsResponse);
rpc ListDelegatedAgentOwners(ListDelegatedAgentOwnersRequest) returns (ListDelegatedAgentOwnersResponse);
rpc RevokeDelegatedAgent(RevokeDelegatedAgentRequest) returns (RevokeDelegatedAgentResponse);
rpc CreateDelegatedSession(CreateDelegatedSessionRequest) returns (CreateDelegatedSessionResponse);
```
Withdrawal initiation, pending-withdrawal listing, and signed calldata lookup.
```protobuf theme={null}
rpc InitiateWithdrawal(InitiateWithdrawalRequest) returns (Withdrawal);
rpc GetWithdrawal(GetWithdrawalRequest) returns (Withdrawal);
rpc ListPendingWithdrawals(ListPendingWithdrawalsRequest) returns (ListPendingWithdrawalsResponse);
```
Fee simulation for prospective orders.
```protobuf theme={null}
rpc SimulateFees(SimulateFeesRequest) returns (SimulateFeesResponse);
```
Testnet token minting for authenticated users.
```protobuf theme={null}
rpc MintTokens(MintTokensRequest) returns (MintTokensResponse);
rpc GetFaucetInfo(GetFaucetInfoRequest) returns (GetFaucetInfoResponse);
```
Public whitelist applications are closed. `SubmitWhitelist` returns `PERMISSION_DENIED` without changing applicant data.
```protobuf theme={null}
rpc SubmitWhitelist(SubmitWhitelistRequest) returns (SubmitWhitelistResponse);
```
Public service health check.
```protobuf theme={null}
rpc Check(PublicHealthCheckRequest) returns (PublicHealthCheckResponse);
```
## Generate a Client
```bash theme={null}
npm install @bufbuild/protobuf @connectrpc/connect @connectrpc/connect-node
brew install bufbuild/buf/buf
```
```yaml theme={null}
# buf.gen.yaml
version: v2
plugins:
- remote: buf.build/bufbuild/es
out: gen/ts
- remote: buf.build/connectrpc/es
out: gen/ts
inputs:
- module: buf.build/monaco-sdk/api:main
```
```bash theme={null}
buf generate
```
```bash theme={null}
pip install grpcio grpcio-tools
brew install bufbuild/buf/buf
```
```yaml theme={null}
# buf.gen.yaml
version: v2
plugins:
- remote: buf.build/protocolbuffers/python
out: gen/python
- remote: buf.build/grpc/python
out: gen/python
inputs:
- module: buf.build/monaco-sdk/api:main
```
```bash theme={null}
buf generate
```
## Authenticate requests
Monaco is **tokenless**. There is no access token to attach: you generate an
ed25519 **session keypair** locally, register its public key during
challenge/verify, and **sign every authenticated request** with the session
private key. The server verifies the signature against the stored public key
(`grpc-api/src/auth/interceptor.rs`); public market-data RPCs need no signature.
`grpcurl` cannot authenticate: the signature is taken over the
**prost-encoded request body**, which grpcurl has no way to produce. Use a
generated gRPC client (any language) and sign each call, as below.
### The signing contract
Each authenticated unary call carries three metadata entries:
| Metadata | Value |
| - | - |
| `x-monaco-publickey` | session public key, lowercase hex |
| `x-monaco-timestamp` | request time in **milliseconds** since epoch, as a decimal string (e.g. `1722945600123`; a seconds value like `1722945600` is rejected as out-of-skew) |
| `x-monaco-signature` | ed25519 signature (lowercase hex) of the canonical string below |
The canonical signing string is four newline-separated lines — note the fixed
`POST`, the **full leading-slash method path**, and the SHA-256 of the exact
serialized request bytes:
```text theme={null}
POST
/./
```
Binding the method path matters: empty-body RPCs all encode to the same bytes,
so without the path a signature captured from a benign call could be replayed
against a destructive one.
### Sign and place an order
The excerpts below are the signing mechanics (`@grpc/grpc-js`); concatenated,
they are a minimal signed create/cancel once you supply a session key and a
trading-pair id.
They load a descriptor set rather than `.proto` files at runtime. Build one from
the public BSR module — no checkout of the Monaco repository is required:
```bash theme={null}
buf build buf.build/monaco-sdk/api -o monaco-descriptors.binpb
```
Rebuild it whenever you pick up a newer version of the module. To verify your
signer against fixed inputs before you send anything, use the
[golden signing vectors](#golden-signing-vectors) below.
Generate a session keypair and register its public key: pass
`session_public_key` to `Challenge`, sign the returned message with your
wallet, then call `Verify`. No token is returned — the session **is** the
registered public key, and `Verify` returns an `expires_at`
(see [Session expiry and refresh](#session-expiry-and-refresh)). The same
handshake over REST — `POST /api/v1/auth/challenge` then
`POST /api/v1/auth/verify`, with the request and response fields spelled
out — is in the [API reference](/api-reference), and
[Wallets & Auth](/developers/accounts/wallets) covers wallet setup.
Serialize the request with the method's own serializer (the exact prost
bytes on the wire), hash them, build the canonical string, and sign it.
```ts theme={null}
import * as grpc from "@grpc/grpc-js";
import * as protoLoader from "@grpc/proto-loader";
import { readFileSync } from "fs";
import { ed25519 } from "@noble/curves/ed25519";
import { sha256 } from "@noble/hashes/sha2";
import { bytesToHex, hexToBytes, utf8ToBytes } from "@noble/hashes/utils";
// ed25519 session private key registered during challenge/verify, as hex.
const rawKey = process.env.MONACO_SESSION_PRIVATE_KEY ?? "";
if (!/^[0-9a-fA-F]{64}$/.test(rawKey)) throw new Error("MONACO_SESSION_PRIVATE_KEY must be 32-byte hex");
const sessionPrivateKey = hexToBytes(rawKey);
const sessionPublicKey = bytesToHex(ed25519.getPublicKey(sessionPrivateKey));
function signedMetadata(method: { path: string; requestSerialize: (v: never) => Buffer | Uint8Array }, request: unknown): grpc.Metadata {
const serialized = method.requestSerialize(request as never);
const bytes = serialized instanceof Uint8Array ? serialized : new Uint8Array(serialized);
const timestamp = String(Date.now());
const hash = bytesToHex(sha256(bytes));
const signingString = ["POST", method.path, timestamp, hash].join("\n");
const signature = bytesToHex(ed25519.sign(utf8ToBytes(signingString), sessionPrivateKey));
const metadata = new grpc.Metadata();
metadata.set("x-monaco-publickey", sessionPublicKey);
metadata.set("x-monaco-timestamp", timestamp);
metadata.set("x-monaco-signature", signature);
return metadata;
}
```
`@grpc/proto-loader` cannot parse the source protos (gnostic aggregate
options), so load a prebuilt `FileDescriptorSet` — `make descriptors-gen`
from a repo checkout, or `buf build buf.build/monaco-sdk/api -o
monaco-descriptors.binpb` against the public schema registry.
```ts theme={null}
const packageDefinition = protoLoader.loadFileDescriptorSetFromBuffer(
readFileSync(new URL("./monaco-descriptors.binpb", import.meta.url)),
{ keepCase: true, longs: String, enums: String, defaults: true, oneofs: true },
);
const proto = grpc.loadPackageDefinition(packageDefinition) as any;
const ordersDef = packageDefinition["monaco.api.orders.OrdersService"] as any;
const orders = new proto.monaco.api.orders.OrdersService("staging.apimonaco.xyz:443", grpc.credentials.createSsl());
const pairId = "TRADING_PAIR_UUID"; // from MarketService.ListTradingPairs
const createOrder = { trading_pair_id: pairId, order_type: "LIMIT", side: "BUY", quantity: "0.01", price: "60000", trading_mode: "SPOT", time_in_force: "GTC" };
orders.CreateOrder(createOrder, signedMetadata(ordersDef.CreateOrder, createOrder), (err: grpc.ServiceError | null, res: any) => {
if (err) return console.error(err.code, err.details);
const cancel = { order_id: res.order_id };
orders.CancelOrder(cancel, signedMetadata(ordersDef.CancelOrder, cancel), (e: grpc.ServiceError | null, r: any) => console.log(e ?? r));
});
```
Each call re-signs: the timestamp and body hash differ per request, so
signatures are never reused.
### Session expiry and refresh
`Verify` and `Refresh` both return `expires_at`, a Unix timestamp (**seconds**)
for when the session lapses. Track it and refresh **proactively, before** it
passes: call `AuthService.Refresh` with an empty `RefreshRequest`, signed exactly
like any other call but over the method path
`/monaco.api.auth.AuthService/Refresh`. It returns a fresh `expires_at`.
```ts theme={null}
const authDef = packageDefinition["monaco.api.auth.AuthService"] as any;
const auth = new proto.monaco.api.auth.AuthService("staging.apimonaco.xyz:443", grpc.credentials.createSsl());
const refreshReq = {}; // RefreshRequest has no fields; the signature still binds the method path
auth.Refresh(refreshReq, signedMetadata(authDef.Refresh, refreshReq), (err: grpc.ServiceError | null, res: any) => {
if (err) return console.error(err.code, err.details); // already expired → re-authenticate
// res.expires_at (seconds): schedule the next refresh before this time.
});
```
Once a session has **already** expired (or been revoked), `Refresh` itself
returns `UNAUTHENTICATED`; at that point you must re-run challenge/verify to
register a new session key. Do **not** loop-retry the same signed request against
a persistent `UNAUTHENTICATED` — it will never succeed. A stale or far-future
`x-monaco-timestamp` is likewise rejected, so always sign with the real current
time in milliseconds, not a cached value.
### Error handling
| Status | Meaning | Action |
| - | - | - |
| `UNAUTHENTICATED` | missing/invalid signature, expired session, or timestamp skew | re-auth (new session), verify the timestamp is current ms, resign |
| `INVALID_ARGUMENT` | malformed request | fix the request; do not retry unchanged |
| `RESOURCE_EXHAUSTED` | rate limited — an order, movement, settings-write, copy trading, registration, read, or public application read budget is spent | read the wait from the `google.rpc.RetryInfo` detail and, on order RPCs, your live budget from the lowercase `x-ratelimit-*` response metadata (reads carry none), back off for that interval, then retry; see [Rate Limits](/developers/rate-limits) |
| `UNAVAILABLE` / `UNKNOWN` with an HTTP `429` in the message | the edge blocked the request — the JSON body's `details.reason` names the rule (`edge_429_flood`, `edge_request_flood`) and `details.retryAfter` the block length | a throttling signal, not a transient fault: back off exponentially with jitter for at least that interval and never retry in a tight loop; see [Rate Limits](/developers/rate-limits#when-you-are-over-budget) |
`CreateOrderRequest.idempotency_key` makes a create safe to replay with the same
normalized payload for 24 hours after acceptance. Generate and persist the key
before signing the first request, then reuse that exact key and payload after an
ambiguous outcome. A different payload returns `ALREADY_EXISTS`; after 24 hours,
the same key may place a new order. Without the retained key — or after it expires —
never blindly retry a `CreateOrder`, or you risk a duplicate order. Reconcile on the order WebSocket stream
rather than `ListOrders`: that read path is served asynchronously from a replica
and can omit an order that did land, and an order rejected *before* acceptance is never written to it at all (one rejected after acceptance is persisted with status REJECTED). The stream is **not replayable** — it carries no sequence number to backfill from — and
its fan-out is non-persistent Core NATS, so the service can re-establish an ended upstream
subscription without closing your socket. Subscribe before you send, but treat only a
matching event as an answer: **silence is never evidence the order did not land**, and no
condition you can observe makes it so. Absent positive evidence, **hold rather than
retry.** Setting a
`clientOrderId` makes the stream events self-identifying, but note it is claimed
only while an order rests — see [Client Order IDs](/developers/trade#client-order-ids).
`ReplaceOrder` behaves differently: it names the original order id, and the
sequencer revalidates that original at sequence time, so once one replacement
succeeds a retry is rejected with `NOT_FOUND` rather than placing a second order.
That code is ambiguous on its own — the original may instead have filled or been
cancelled — so reconcile to find the replacement's new order id rather than to
rule out a duplicate.
### Golden signing vectors
Deterministic vectors — request, serialized bytes, timestamp, canonical string,
and signature for a spot and a margin `CreateOrder`. They are the source of
truth for the signing contract and are exercised by both the TypeScript example
tests and a server-side Rust test; the copy below is drift-gated against the one
those tests load, so it cannot fall out of date.
No private key is shipped, so an implementation in any language can reproduce
the byte-identical request bytes and canonical signing string and **verify** each
published signature against `signerPublicKey` — the check the gateway itself
performs. Producing your own signatures needs your own session key; these
vectors prove the bytes and verification, not re-signing. Pin them in your
signer's unit tests: serialize each `request` with a descriptor set built from
`buf.build/monaco-sdk/api`, and assert you reproduce `serializedHex`,
`bodySha256Hex`, and `signingString` before checking `signature` against
`signerPublicKey`.
```json Golden signing vectors expandable theme={null}
{
"methodPath": "/monaco.api.orders.OrdersService/CreateOrder",
"timestampMs": 1700000000000,
"signerPublicKey": "fff86b85a717d5b1564aa6047550094eb150b5fc4ee1ec9de783923c0cb32ef3",
"vectors": {
"spot": {
"request": {
"trading_pair_id": "afae0e16-2d05-4ee9-9ee8-afae0e162d05",
"order_type": "LIMIT",
"side": "BUY",
"quantity": "0.01",
"price": "60000",
"trading_mode": "SPOT",
"time_in_force": "GTC"
},
"serializedHex": "0a2461666165306531362d326430352d346565392d396565382d61666165306531363264303512054c494d49541a03425559220536303030302a04302e3031320453504f545203475443",
"bodySha256Hex": "22ba7932ddf7d17a79d341f46557a2bfa02297710356d8da06d83e234a56d6b8",
"signingString": "POST\n/monaco.api.orders.OrdersService/CreateOrder\n1700000000000\n22ba7932ddf7d17a79d341f46557a2bfa02297710356d8da06d83e234a56d6b8",
"signature": "6f27e6bd39fe3b4c942a762111e9b23217252e0260f1b0148c182f9c72ef43f7c0b8c340089f01e04c614c5b4186ddd43ed4e5e83f8d0ea6ff0c9eab309e6e03"
},
"margin": {
"request": {
"trading_pair_id": "123e4567-e89b-12d3-a456-426614174000",
"order_type": "LIMIT",
"side": "BUY",
"quantity": "0.01",
"price": "60000",
"trading_mode": "MARGIN",
"margin_account_id": "123e4567-e89b-12d3-a456-426614174000",
"leverage": "5",
"time_in_force": "GTC"
},
"serializedHex": "0a2431323365343536372d653839622d313264332d613435362d34323636313431373430303012054c494d49541a03425559220536303030302a04302e303132064d415247494e52034754435a2431323365343536372d653839622d313264332d613435362d3432363631343137343030306a0135",
"bodySha256Hex": "fff189b1eb156eaa41e89a1cecdd1d365d27f6f219f3b5017bcf8e2f31666eb9",
"signingString": "POST\n/monaco.api.orders.OrdersService/CreateOrder\n1700000000000\nfff189b1eb156eaa41e89a1cecdd1d365d27f6f219f3b5017bcf8e2f31666eb9",
"signature": "348b35353535f3158f67a1d9ecde090bcd483eee0f92b3fc9b1e3f42f75dfa6c1a9ffcb58ed638d5e6f29db3844e75bd3a2e7e9241214e247b345fb20614b100"
}
}
}
```
The `signingString` values above render `\n` as literal backslash-n inside JSON;
the real canonical string joins its four parts with newline characters.
Signatures are ed25519 over the UTF-8 bytes of that string, hex-encoded.
# HTTP message signatures
Source: https://docs.0xmonaco.com/reference/http-message-signatures
RFC 9421 session-key signing profile and migration from legacy REST authentication.
Monaco supports an **opt-in RFC 9421 signing protocol** alongside legacy
`X-Monaco-*` authentication. Both use the same wallet-authorized Ed25519 session
keys. Deploy server support before opting clients in. Existing clients and
sessions continue to work; legacy authentication is not retired.
## What the signature protects
The signature binds the HTTP method, escaped path, raw query, and exact body
bytes to a registered session key. Changing an order's price or quantity, query
parameter ordering, percent encoding, or JSON whitespace invalidates it.
Use HTTPS: signatures provide integrity and key possession, not confidentiality.
The gateway remains trusted to verify signatures, resolve wallet-to-session-key
bindings, and enforce authorization. Downstream services do not independently
verify the original HTTP signature. This protocol does not make a compromised
gateway unable to fabricate internal commands. Host and scheme are not covered;
keys must not be reused across independently trusted deployments. These trust
boundaries also apply to legacy request signing.
Session expiry, revocation, delegated-agent permissions, and account restrictions
continue to use the existing shared authentication checks. Register a new session
key through the existing wallet flow for rotation, migrate clients, then revoke
the old session. WebSocket and gRPC signing are unchanged.
## Signing profile
This is an application profile of [RFC 9421](https://www.rfc-editor.org/rfc/rfc9421.html)
using [RFC 9530 Content-Digest](https://www.rfc-editor.org/rfc/rfc9530.html).
It is not a general-purpose verifier for arbitrary HTTP signature profiles.
Every signed request, including GET and empty-body requests, sends:
```http theme={null}
Content-Digest: sha-256=:BASE64_SHA256_OF_BODY:
Signature-Input: monaco=("@method" "@path" "@query" "content-digest");created=1788868800;keyid="SESSION_PUBLIC_KEY_HEX";alg="ed25519"
Signature: monaco=:BASE64_ED25519_SIGNATURE:
```
* The signature label is `monaco`; exactly one dictionary member is accepted.
The four covered components appear in the order shown. `created`, `keyid`,
and `alg` are required; their ordering may vary. Extra components, parameters,
signature labels, or digest algorithms are rejected. Structured-field spaces
are accepted and signature parameters are serialized canonically. Raw header
limits are 1,024 bytes for `Signature-Input` and 128 bytes each for `Signature`
and `Content-Digest`.
* `created` is an integer Unix timestamp in **seconds**, within 30 seconds of
server time. Legacy timestamps use milliseconds.
* `keyid` is the registered 32-byte session public key, encoded as 64 lowercase
hexadecimal characters. It is a lookup identifier, not a remote key URL.
* `alg` is exactly `ed25519`.
* `@method` is the uppercase request method. `@path` is the escaped path as sent.
* `@query` starts with `?` and retains raw ordering, duplicates, and encoding.
Its value is `?` when the query is absent or empty. Do not decode or sort it.
* `content-digest` covers SHA-256 of the exact body bytes, encoded as a structured
byte sequence. Hash the empty byte string when there is no body.
Two worked `@query` values, since divergent canonicalization is the most common
cause of an otherwise-correct signature failing:
* `GET /api/v1/orders` signs `@path` as `/api/v1/orders` and `@query` as `?`.
* `GET /api/v1/orders?status=FILLED%2CPARTIALLY_FILLED&pageSize=20` signs `@query`
as `?status=FILLED%2CPARTIALLY_FILLED&pageSize=20` — exactly the bytes on the
wire, neither percent-decoded nor reordered. Sending the same filter unencoded,
as `?status=FILLED,PARTIALLY_FILLED&pageSize=20`, is a different signature base
even though the server reads the same filter, so sign the string you actually
send.
The signature base has one line per component followed by the signature
parameters. There is **no trailing newline**:
```text theme={null}
"@method": POST
"@path": /api/v1/orders
"@query": ?tag=a%2Fb&tag=a+b
"content-digest": sha-256=:BASE64_SHA256_OF_BODY:
"@signature-params": ("@method" "@path" "@query" "content-digest");created=1788868800;keyid="SESSION_PUBLIC_KEY_HEX";alg="ed25519"
```
Sign the UTF-8 bytes of that base with the session private key. Send the same
body bytes that were hashed. Monaco rejects missing coverage, invalid digests,
unsupported algorithms, ambiguous headers, and mixed RFC/legacy credentials.
Presence of `Signature` or `Signature-Input` selects standard verification; failure
never falls back to legacy authentication.
`Content-Digest` alone is ordinary HTTP integrity metadata, not a signature
credential. It does not select RFC authentication or override the body hash
verified by legacy authentication.
## Freshness, retries, and idempotency
The 30-second clock window limits how long a captured request can be reused.
**It does not prevent an identical signed request from being accepted again
inside that window.** Monaco keeps no nonce or distributed replay
cache. A fresh signature is also not proof that an operation is new.
Use the endpoint's documented idempotency mechanism where available. Do not
blindly retry writes without it. Re-sign a retry if its timestamp has aged out,
while retaining the original operation's idempotency key and payload. Endpoints
without idempotency support provide no exactly-once guarantee.
## TypeScript migration
After server deployment, opt in when constructing the SDK:
```typescript theme={null}
const sdk = createMonacoSDK({
network: "staging",
seiRpcUrl: "https://your-sei-rpc.example",
requestSigning: "rfc9421",
});
```
The default remains `"legacy"`. RFC signing covers the final URL and serialized
string body. The SDK manages all signature headers and rejects caller overrides
or unsupported body types. RFC-signed requests disable redirects, including caller
overrides. Public requests keep their existing
authentication. No automatic protocol downgrade occurs on errors.
## Rust migration
The Rust REST SDK signs generated session-authenticated operations using the
final request URL and serialized body. Existing unsigned constructors keep their
signatures. Public operations remain unsigned.
```rust theme={null}
use monaco_sdk::{Client, RequestSigning, SessionSigner};
// The seed belongs to a session already registered through wallet authentication.
let signer = SessionSigner::new(session_private_seed, RequestSigning::Rfc9421);
let client = Client::new_authenticated("https://staging.apimonaco.xyz", signer);
```
Select `RequestSigning::Legacy` for a server that has not migrated. Authenticated
clients disable HTTP redirects. The low-level `SessionSigner::sign_at` helper is
also available for reusable reqwest requests; its caller must preserve the signed
bytes and disable redirects. Streaming bodies are rejected rather than silently
signed as empty.
## Deployment sequence
1. Deploy the server accepting both protocols and verify signed reads and writes
with an existing session key in staging.
2. Verify stale timestamps, query/body tampering, revoked sessions, and malformed
signatures fail. Confirm public routes bypass auth buffering.
3. Release SDKs and opt individual applications in. Keep old clients on legacy
until their migration is verified. Client rollback selects `legacy`; do not
roll the server back while RFC-only clients still target it.
4. Retire legacy signing only through a separate coordinated contract change
after deployed-client usage is known. No retirement date is set.
Signed routes still read and hash their bodies and enforce a 2 MiB bound. Public
routes use their normal extractor limits rather than the signed
body buffer. This change reduces unnecessary global buffering; it does not remove
body hashing from trading requests or promise a trading-latency improvement.
# WebSocket API
Source: https://docs.0xmonaco.com/reference/websockets
Listen to live market data and authenticated account events over raw WebSockets
## Endpoint
```text theme={null}
wss://ws-staging.apimonaco.xyz/ws
```
TypeScript SDK helpers and React hooks are available for WebSocket subscriptions. Use them when you want typed channel helpers, reconnect handling, and cleanup helpers. See [TypeScript WebSockets](/sdk/typescript/websockets) and [React hooks](/sdk/react/intro).
## Connection Setup
* Open one connection to the WebSocket endpoint and reuse it for all subscriptions.
* Subscribe with channel strings; unsubscribe with the same channel strings.
* Channel parameters use trading pair UUIDs, not symbols.
* Public market channels do not require authentication.
* Account channels require a session-key WebSocket authentication message before subscribing.
* Use `*` only where listed to leave a filter open.
## Message Format
Subscribe and unsubscribe with the same channel strings.
```json theme={null}
{
"type": "Subscribe",
"channels": ["trades:550e8400-e29b-41d4-a716-446655440000"]
}
```
The server answers each `Subscribe` with one `Subscribed` acknowledgement listing **only the channels that request changed** — the ones it actually added, in the order you sent them.
```json theme={null}
{
"type": "Subscribed",
"channels": ["trades:550e8400-e29b-41d4-a716-446655440000"]
}
```
* A channel the connection already holds is a no-op — it is not re-subscribed, no snapshot is replayed, and it is **left out of the ack**.
* A channel that is malformed, fails validation, requires authentication you have not completed, or exceeds the per-connection subscription cap gets its own `Error` frame (with a machine-readable `code` and the rejected `channel`) and is **left out of the ack**. The ack still follows, so a partially-accepted request yields the error frames first and then the accepted subset.
* `"channels": []` therefore means the request added nothing, whatever the reason: every channel was rejected, you already held them all, some mix of the two, or the request named no channels at all.
A per-channel `Error` frame names the channel it concerns in `channel`, echoed exactly as you sent it — including a string the server could not parse — so you can pair each rejection with your request without reading the human-readable `message`. A channel longer than 128 bytes (never a valid channel) is echoed as its first 128 UTF-8 bytes, cut at a character boundary:
```json theme={null}
{
"type": "Error",
"code": "AUTH_REQUIRED",
"message": "Channel 'orders' requires authentication. Please authenticate first.",
"channel": "orders"
}
```
`INVALID_SUBSCRIPTION`, `AUTH_REQUIRED`, `SUBSCRIPTION_LIMIT`, and `SNAPSHOT_UNAVAILABLE` carry a `channel`. A `SNAPSHOT_UNAVAILABLE` channel was accepted — it is in the ack and its live frames flow — so its `channel` tells you which baseline to refetch. Connection-wide failures (`AUTH_FAILED`, `CONNECTION_LIMIT`, `INVALID_MESSAGE`, `SESSION_INVALID`, `MESSAGE_RATE_LIMIT`) concern no single channel and have no `channel` key.
Do not read the ack as the connection's full subscription list; it is the delta for that one request. Track what you hold client-side.
Authenticated channels require a session-key handshake first. The signature covers `WS-AUTH\n\n` with the session private key.
```json theme={null}
{
"type": "Authenticate",
"public_key": "",
"timestamp": 1710000000000,
"signature": ""
}
```
`timestamp` is epoch milliseconds and is bound into the signature. The server rejects a handshake whose timestamp is outside a ±30s clock-skew window (stale or future-dated), so sign with the current time and re-sign on every reconnect — never cache and replay an old `Authenticate` payload.
Events are delivered in this envelope:
```json theme={null}
{
"type": "Event",
"channel": "trades:550e8400-e29b-41d4-a716-446655440000",
"data": {}
}
```
### Heartbeat (Ping/Pong)
The connection uses application-level heartbeats. A client may send `{"type":"Ping"}` and the server replies `{"type":"Pong"}`. The server may also send `{"type":"Ping"}`, and the client must reply with `{"type":"Pong"}` to keep the connection alive.
```json theme={null}
{ "type": "Ping" }
```
### Slow-client disconnect (close code 1013)
When the server detects that a connection's broadcast receiver missed events that one of its subscriptions owns (see below), it closes the **whole connection**, including every other subscription on it, with code `1013` ("try again later") and reason `slow consumer: stream gapped, resync required`. This close frame is the machine-readable resync signal; no additional JSON `Error` frame precedes it. Sending the close is best-effort: a broken socket may surface only an abnormal transport close, and still releases server resources.
The same close, with the same reason, is also sent when events may have been lost before they reached the WebSocket server: the matching engine dropped a frame under load, a publish failed, or the server's NATS connection dropped, reconnected, or fell behind. Which events were lost is unknown, so every connection on that server that holds at least one subscription and was open when the events were lost is closed, whatever it subscribes to; a connection with no subscription, or one opened after the loss (for example during a matching engine restart), stays open. These closes are spaced at least about one second apart, so a burst of losses closes a connection once. Periodic frames whose next frame restates their full state (`market_stats` and position valuation frames) never cause one: a later frame repairs a lost one.
A gap closes the connection only if one of its subscriptions owns it. A subscription to `orders`, `conditional_orders`, `twap_orders`, `positions`, `balances`, `account`, `instrument`, `liquidations`, `copy`, or a single market's `market_stats:` owns every gap, because its subscribe snapshot may not yet reflect an event that the gap overwrote. A subscription to any other channel (`trades`, `orderbook`, `ohlcv`, `movements`, all-markets `market_stats`) owns only gaps that reach events published after its `Subscribed` acknowledgement. An earlier gap just means its stream starts after it. For example, a public market-data connection that falls behind while its handshake or an earlier `Subscribe` is still being processed keeps streaming instead of reconnecting into the same backlog.
The ring drops arbitrary events, not superseded values for each key. A lost position close, candle, balance, or instrument update may have no later replacement. Even a connection holding only absolute-state channels must reconnect and reconcile. Healthy peers continue consuming independently; NATS delivery remains at-most-once and is not replayed on reconnect.
Reconnect, authenticate, and resubscribe before reconciling REST reads with incoming events. Subscribe snapshots exist for `balances`, `orders`, `conditional_orders`, `twap_orders`, `liquidations`, `positions`, `instrument`, `account`, `copy`, and `market_stats` (per-market and all-markets); they describe current state, not every missed lifecycle transition. A per-market `trades` subscription also gets one: the market's newest 50 trades, a recent-history baseline rather than current state (see [Trades](#trades)). So does an `ohlcv` subscription to one market, interval and price series: its newest 350 candles, also a recent-history baseline (see [OHLCV](#ohlcv)). Apply each channel's baseline semantics: complete maps such as positions and instruments can replace their prior set. For persisted resting orders, merge by per-order `version`; snapshot absence is not a tombstone because persistence can lag a newer local event. Retain such local rows and resolve them with `getOrder` before applying the version rule. An order `version` is not a contiguous replay cursor. See the [SDK reconciliation guidance](/sdk/typescript/websockets#reconnection-and-resync).
The remaining channels do **not** provide a subscribe-time snapshot — the append-only `movements` stream and `orderbook` (whose first live frame is already the full book). Applications must explicitly refresh the relevant REST state/history, including quiet keys that may never emit again. A per-market `market_stats` subscription whose market has not yet produced a frame (or on a transient matching-engine read failure), and the all-markets channel for a brief window during initial startup or after a server restart (until the producer commits its first full generation, or on the same read failure), return `SNAPSHOT_UNAVAILABLE` instead of a baseline — treat it as a transient resync signal, not a failure: keep the subscription open (no auth or session reset), and let a later published live frame seed the baseline once one is produced. Account keepalives cover active accounts only, so a flat account whose collateral does not move stays silent on the live stream — the `account` subscribe snapshot includes it, and so establishes a complete baseline **for the authenticated application**. It is not a full replacement set for the channel: live `account` delivery is user-scoped and can carry accounts from other applications (see [Account](#account)), which the snapshot deliberately omits. Merge it per `margin_account_id` rather than replacing the whole channel map, or those cross-application keys are discarded. A resumed stream alone does not establish a complete baseline for the other channels.
For additive history such as public trades and movements, use the available REST history and its documented pagination/retention limits. The `trades` snapshot covers only the newest 50 trades, and the `ohlcv` snapshot only the newest 350 candles; neither is a gap-free replay. There is no universal WebSocket replay cursor or guarantee that an arbitrary outage can be reconstructed completely. Surface an unrecovered gap when the required history is unavailable; do not assume a fresh current-state snapshot reproduces missed events.
The TypeScript SDK automatically reconnects when enabled and invokes `onResync({ code: 1013, slowClient: true })` after **sending** authentication/subscription requests. It does not wait for acknowledgements or snapshots and does not automatically perform REST refetches; the application owns recovery.
### Inbound message rate (close code 1008)
Every application frame you send — `Authenticate`, `Subscribe`, `Unsubscribe`, `Ping`, or anything unparseable — draws from a per-connection budget. The defaults are **20 messages per second sustained with a burst of 1,200**; both values are operator-configurable, so a deployment may run different numbers. Protocol-level `Ping` frames count too (each costs the server a `Pong` write), and so does every fragment of a fragmented message; protocol `Pong` (your reply to the server heartbeat) and `Close` are not counted. Frames are also metered by size: a per-connection byte budget admits a full re-subscribe frame at connect plus normal traffic, but refuses a burst of very large frames even well under 20 per second. The first frame past either budget receives an `Error` frame with code `MESSAGE_RATE_LIMIT`, and the connection is then closed with `1008` ("policy violation") and reason `message rate limit exceeded` (or `message byte limit exceeded` when the byte budget ran out). The budget is per connection, so it 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 \~25 s heartbeat idle. The TypeScript SDK sends every held channel in a single `Subscribe` frame when the socket opens and on every reconnect; a channel added while the socket is already open is sent as its own frame, and the default burst (1,200) is sized above the default subscription cap (1,024 channels) so even adding a full subscription set one channel at a time on a live connection stays within budget. The SDK reconnects through `1008` with backoff and re-sends its authentication and subscriptions; an application that re-subscribes in a hot loop on every reconnect will trip the cap again, so treat `MESSAGE_RATE_LIMIT` in `onError` as a signal to slow down, not to retry immediately.
## Connect
```typescript theme={null}
const pairId = "550e8400-e29b-41d4-a716-446655440000";
const ws = new WebSocket("wss://ws-staging.apimonaco.xyz/ws");
ws.addEventListener("open", () => {
ws.send(JSON.stringify({
type: "Subscribe",
channels: [
`trades:${pairId}`,
`orderbook:${pairId}:SPOT:0.01:base`,
],
}));
});
ws.addEventListener("message", (message) => {
const payload = JSON.parse(message.data);
if (payload.type !== "Event") return;
console.log(payload.channel, payload.data);
});
```
```typescript theme={null}
import WebSocket from "ws";
const pairId = "550e8400-e29b-41d4-a716-446655440000";
const ws = new WebSocket("wss://ws-staging.apimonaco.xyz/ws");
ws.on("open", () => {
ws.send(JSON.stringify({
type: "Subscribe",
channels: [`trades:${pairId}`],
}));
});
ws.on("message", (message) => {
const payload = JSON.parse(message.toString());
if (payload.type === "Event") {
console.log(payload.channel, payload.data);
}
});
```
```python theme={null}
import asyncio
import json
import websockets
PAIR_ID = "550e8400-e29b-41d4-a716-446655440000"
URL = "wss://ws-staging.apimonaco.xyz/ws"
async def main():
async with websockets.connect(URL) as ws:
await ws.send(json.dumps({
"type": "Subscribe",
"channels": [f"trades:{PAIR_ID}"],
}))
async for message in ws:
payload = json.loads(message)
if payload.get("type") == "Event":
print(payload["channel"], payload["data"])
asyncio.run(main())
```
To stop a stream, send `Unsubscribe` with the same channel string and close the socket when no subscriptions remain.
```json theme={null}
{
"type": "Unsubscribe",
"channels": ["trades:550e8400-e29b-41d4-a716-446655440000"]
}
```
`Unsubscribed` mirrors `Subscribed`: it lists only the channels that request actually removed. A channel the connection was not subscribed to is an idempotent no-op and is left out, and a channel string the server cannot parse gets an `INVALID_SUBSCRIPTION` `Error` frame whose `channel` is that string, exactly as you sent it.
```json theme={null}
{
"type": "Unsubscribed",
"channels": ["trades:550e8400-e29b-41d4-a716-446655440000"]
}
```
## Channel Catalog
### Public Channels
| Channel | Pattern | Event | Use for |
| - | - | - | - |
| Trades | `trades:{trading_pair_id}` | `trade` | Live executions for one pair or every pair |
| Orderbook | `orderbook:{trading_pair_id}:{mode}:{magnitude}:{denomination}` | `orderbook_update` | Live bid/ask depth |
| OHLCV | `ohlcv:{trading_pair_id}:{mode}:{interval}[:{price_type}]` | `ohlcv_update` | Live candle updates (trade or `mark` series) |
| Market stats (per-market) | `market_stats:{trading_pair_id}` | — (routed by `channel`) | Periodic per-market statistics: prices, best bid/ask, open interest, 24h stats, funding state |
| Market stats (all markets) | `market_stats` / `market_stats:*` | — (routed by `channel`) | Slower-cadence reduced summary for every published market (a market that has not yet assembled its first frame is omitted) |
| Instrument | `instrument:{trading_pair_id}` / `instrument` / `instrument:*` | — (routed by `channel`) | Trading-pair lifecycle + config: listings, delistings, and tick/step/order-size/category edits; each frame carries the full market config (perp leverage/margin included) and the base/quote asset icon URLs |
### Authenticated Channels
| Channel | Pattern | Event | Use for |
| - | - | - | - |
| Orders | `orders:{trading_pair_id}:{mode}` | order lifecycle | User order placement, fill, cancel, reject, and expiry events |
| Balances | `balances` | `balance_update` | Balance state changes |
| Movements | `movements` | `user_movement` | Ledger entries for deposits, withdrawals, funding settlements, and the unlocks a cancelled or expired order releases — **not** fills or fees |
| Positions | `positions:{trading_pair_id}` | `position_update`, `position_valuation` | Perp position changes, and periodic mark-derived valuations of open positions |
| Conditional Orders | `conditional_orders:{trading_pair_id}` | `conditional_order_update` | Take-profit, stop-loss and trailing-stop lifecycle updates |
| TWAP Orders | `twap_orders:{trading_pair_id}` | `twap_order_update` | TWAP parent-order progress: creation, slice placements/skips, passive-child mutations, conditional activation/expiry, cancellation, completion |
| Liquidations | `liquidations:{trading_pair_id}` | `liquidation_alert` | Liquidation-risk alerts and status changes |
| Account | `account` | `account_update` | Margin-account health: equity, collateral, margin ratio, maintenance requirement |
| Copy | `copy` | `copy_relationship_update`, `copy_skip` | Your copy trading relationships as a follower: status changes and leader fills that were not mirrored |
## Channel Details
### Trades
Executed trades for one pair, including price, quantity, maker side, and execution time.
```text theme={null}
trades:{trading_pair_id}
```
Trading pair ID. Use `*` only when you intentionally want trades for every pair.
**Example**
```text theme={null}
trades:550e8400-e29b-41d4-a716-446655440000
```
#### Subscribe-time snapshot
On subscribe to a single market, before any live trade, the server sends a `Snapshot` frame whose `data` is an array of that market's newest **50** trades, **newest first** — the same trades, in the same order, as the first page of the REST trades read with `pageSize` 50. Each row is the live trade frame's inner `data` object (`trade_id`, `price`, `quantity`, `quantity_raw`, `maker_side`, `executed_at`); the market is the channel's, so the `event_type`, `trading_pair_id` and `trading_mode` envelope is not repeated per row. Decimal values are JSON strings. A market with no trades, or an unknown `trading_pair_id`, gets an empty array. A `trades:*` subscription gets no snapshot. A read failure returns `SNAPSHOT_UNAVAILABLE` and leaves the subscription active.
```json theme={null}
{
"type": "Snapshot",
"channel": "trades:550e8400-e29b-41d4-a716-446655440000",
"data": [
{
"trade_id": "123e4567-e89b-12d3-a456-426614174001",
"price": "35000.5",
"quantity": "0.5",
"quantity_raw": "50000000",
"maker_side": "SELL",
"executed_at": "2026-10-09T12:34:57.120Z"
},
{
"trade_id": "123e4567-e89b-12d3-a456-426614174000",
"price": "35000",
"quantity": "1.25",
"quantity_raw": "125000000",
"maker_side": "BUY",
"executed_at": "2026-10-09T12:34:56.789Z"
}
],
"timestamp": "2026-10-09T12:35:00.002Z"
}
```
The snapshot is a recent-history baseline, not a gap-free replay. It reads recorded trades, which can trail the live stream by a few seconds, so a trade can appear both in the snapshot and in an early live frame, and a trade executed just before you subscribed can be in neither. Dedupe on `trade_id` when merging the snapshot with live frames, and use the REST trades read for anything older or for a complete history.
### Orderbook
Live bid/ask depth with optional aggregation and base or quote quantity formatting.
```text theme={null}
orderbook:{trading_pair_id}
orderbook:{trading_pair_id}:{mode}
orderbook:{trading_pair_id}:{mode}:{magnitude}
orderbook:{trading_pair_id}:{mode}:{magnitude}:{denomination}
```
Trading pair ID. Use `*` for every pair.
Trading mode filter. Omit it for all modes, or use `*` when keeping later segments.
Price-level grouping. Supported values are `0.0001`, `0.001`, `0.01`, `0.1`, `1`, `10`, `100`, `1000`, `10000`, and `*`.
Quantity denomination. `base` returns base-token quantities; `quote` returns quote-token notional quantities.
**Example**
```text theme={null}
orderbook:550e8400-e29b-41d4-a716-446655440000:SPOT:0.01:base
orderbook:550e8400-e29b-41d4-a716-446655440000:SPOT:*:quote
```
### OHLCV
Live candle updates for charting and market analytics.
```text theme={null}
ohlcv:{trading_pair_id}:{mode}:{interval}
ohlcv:{trading_pair_id}:{mode}:{interval}:{price_type}
```
Trading pair ID. Use `*` for every pair.
Trading mode filter.
Candle interval.
Optional price series. Omit it (the four-token form) or use `trade` for the
trade-derived candles. `mark` selects the close-only mark-price series (open =
high = low = close = mark, zero volume), published on `1m` for margin pairs.
Use `*` to receive both series. A four-token subscription receives only the
trade series.
**Example**
```text theme={null}
ohlcv:550e8400-e29b-41d4-a716-446655440000:SPOT:1m
ohlcv:550e8400-e29b-41d4-a716-446655440000:MARGIN:1m:mark
```
#### Subscribe-time snapshot
On subscribe to one market, interval and price series, before any live candle, the server sends a `Snapshot` frame whose `data` is an array of that series' newest **350** candles, **oldest first** — the same candles, in the same order, as the REST candlesticks read with `limit` 350 and the same interval and `priceType`, including its flat candles for periods with no trades. Each row is the live candle frame's inner `data` object (`open`, `high`, `low`, `close`, `volume`, `quote_volume`, `trades_count`, `period_start`, `period_end`); the channel fixes the market, interval and series, so the envelope is not repeated per row. Decimal values are JSON strings, `period_start` and `period_end` are Unix milliseconds, and `period_end` is `period_start` plus the interval, as on a live candle. `quote_volume` is `null` for a candle whose quote volume was not recorded.
```json theme={null}
{
"type": "Snapshot",
"channel": "ohlcv:550e8400-e29b-41d4-a716-446655440000:SPOT:1m",
"data": [
{
"open": "35000.00000000",
"high": "35020.50000000",
"low": "34990.00000000",
"close": "35010.00000000",
"volume": "1.25000000",
"quote_volume": "43760.12500000",
"trades_count": 7,
"period_start": 1760011920000,
"period_end": 1760011980000
},
{
"open": "35010.00000000",
"high": "35015.00000000",
"low": "35005.00000000",
"close": "35012.00000000",
"volume": "0.40000000",
"quote_volume": "14004.40000000",
"trades_count": 3,
"period_start": 1760011980000,
"period_end": 1760012040000
}
],
"timestamp": "2026-10-09T12:14:05.002Z"
}
```
* The four-token form is the `trade` series. A `mode` of `*` is served.
* A `*` market, interval or price type gets no snapshot, because one frame carries one series.
* An unknown `trading_pair_id`, a `mode` that is not the market's (`SPOT` on a margin market, or `MARGIN` on a spot market), and `mark` on a spot market get an empty array: no live candle comes for them.
* `mark` is served on every interval listed above, but live mark updates stream on `1m` only, so a `mark` snapshot on another interval is a baseline with no live updates after it.
* A read failure returns `SNAPSHOT_UNAVAILABLE` and leaves the subscription active.
The snapshot is a recent-history baseline, not a gap-free replay. It reads recorded candles, which trail the live candles: the newest `1m` candle is the last closed minute, and longer intervals can lag, so the snapshot's newest candle can be partial or missing. A live candle is the full current state of its period. Upsert by `period_start` when merging: a live candle replaces the snapshot candle with the same `period_start`, or appends a newer one. Use the REST candlesticks read for anything older.
### Market stats
Periodic market statistics, public (no session handshake). A **per-market** subscription delivers frames with index / mark / mid prices, best bid/ask, open interest and its limit, last trade price, 24h volume / high / low and absolute and percentage price change, funding state (current rate, estimated next rate, next funding timestamp, funding interval, last funding time, premium, and clamp bounds), and a `market_status` label. Subscribing to the bare `market_stats` channel (or `market_stats:*`) instead delivers, on a slower cadence, a single frame carrying a reduced per-market summary for every published market — a market that has not yet assembled its first frame is omitted, and the subscribe-time snapshot carries the same set — including a `market_status` label (`TRADABLE`, `POST_ONLY`, `HALTED`, `NOT_READY`, or `UNKNOWN`). A market's per-market frame and its all-markets row carry the same label.
SPOT markets flow through both channels carrying orderbook, last trade, 24h stats, and a `TRADABLE` / `HALTED` label; the perp-only fields (index/mark price, open interest and limit, funding rate/timestamp/interval/last funding time/premium, base interest rate, funding clamps) are omitted for spot.
On subscribe, before any live frame, the server sends a `Snapshot` frame carrying the channel's current baseline as an array: for a per-market subscription, one element with that market's latest complete frame (the same shape as a live per-market `data`); for the all-markets channel, one reduced summary per active market with retained data, ordered by trading-pair id (the same shape as the live `markets` elements). A per-market subscription whose market has not yet produced a frame — or on a transient matching-engine read failure — receives a `SNAPSHOT_UNAVAILABLE` error instead of a fabricated frame, and the subscription stays active so a later published live frame seeds it once the market produces one. The all-markets channel returns `SNAPSHOT_UNAVAILABLE` in the same way for a brief window during initial startup or after a server restart (until the producer commits its first full generation), and on any transient snapshot read failure against the matching engine, rather than a partial baseline; treat it as a resync signal and let the next periodic all-markets push deliver the baseline. Both channels stay active through it. Where a deployment does not run the market-stats snapshot service, neither channel sends a snapshot or a `SNAPSHOT_UNAVAILABLE` frame at all — the first live push seeds state, so do not wait on either.
```text theme={null}
market_stats:{trading_pair_id}
market_stats
market_stats:*
```
Trading pair ID for a per-market subscription. Omit the segment (or use `*`) for the all-markets summary.
**Example**
```text theme={null}
market_stats:550e8400-e29b-41d4-a716-446655440000
```
Frames are routed by `channel` — there is no inner event-type discriminator. Every decimal price/quantity is a JSON **string**; `funding_timestamp` and `last_funding_time` are numeric millisecond epochs; `funding_interval_seconds` is a whole number of seconds; keys are snake\_case; optional fields are **omitted** (not `null`) when absent.
**Per-market frame** (`market_stats:{trading_pair_id}`) — the envelope `data` carries a `trading_pair_id` and a nested `data` object. Perp-only fields (`index_price`, `mark_price`, `open_interest`, `open_interest_limit`, `open_interest_headroom`, `current_funding_rate`, `funding_rate`, `funding_timestamp`, `funding_interval_seconds`, `last_funding_time`, `premium`, `base_interest_rate`, `funding_clamp_small`/`funding_clamp_big`) are omitted for spot markets. `market_status` is present on every frame, perp and spot: `TRADABLE`, `POST_ONLY`, `HALTED`, `NOT_READY`, or `UNKNOWN` for perps, `TRADABLE` or `HALTED` for spot. `funding_interval_seconds` is the funding settlement interval. `last_funding_time` is when the latest funding window settled, as a millisecond epoch (REST `getFundingState` returns the same instant as an RFC 3339 string); it is omitted until the market's first settlement. `open_interest` is in base units; `open_interest_limit` (the market's gross open-interest cap) and `open_interest_headroom` (`open_interest_limit − open_interest × mark_price`, never below zero) are USD notional and present only when the market has a cap. Until a perp's open interest is known, `open_interest` and `open_interest_headroom` are omitted (never zero) while every other field is still sent; once known, every frame carries the most recent value. At zero headroom, orders that open exposure are refused until open interest or the mark falls:
```json theme={null}
{
"channel": "market_stats:550e8400-e29b-41d4-a716-446655440000",
"data": {
"trading_pair_id": "550e8400-e29b-41d4-a716-446655440000",
"data": {
"symbol": "BTC/USDC",
"trading_pair_id": "550e8400-e29b-41d4-a716-446655440000",
"index_price": "64000.0",
"mark_price": "64010.5",
"mid_price": "64008.0",
"best_bid_price": "64005.0",
"best_ask_price": "64011.0",
"open_interest": "128.5",
"open_interest_limit": "10000000",
"open_interest_headroom": "1774650.75",
"last_trade_price": "64009.0",
"daily_base_token_volume": "842.1",
"daily_quote_token_volume": "53900000.0",
"daily_price_low": "62800.0",
"daily_price_high": "64850.0",
"daily_price_change": "1200.0",
"daily_price_change_percent": "1.91",
"current_funding_rate": "0.0001",
"funding_rate": "0.00012",
"funding_timestamp": 1735790400000,
"premium": "0.00005",
"base_interest_rate": "0",
"funding_clamp_small": "0.0005",
"funding_clamp_big": "0.0005",
"market_status": "TRADABLE",
"funding_interval_seconds": 3600,
"last_funding_time": 1735786800000
}
}
}
```
**All-markets summary frame** (`market_stats` / `market_stats:*`) — the envelope `data` carries a `markets` array of reduced per-market summaries, each with a `market_status` label. `index_price`, `funding_rate`, `premium` and `open_interest_notional` are perp-only and omitted for spot. `funding_rate` is the estimated next-window rate (`current_funding_rate` is the last settled one). `open_interest_notional` is `open_interest` × `mark_price`, falling back to the last trade price when there is no mark, in quote units; it is omitted until both open interest and a price are known, never sent as zero:
```json theme={null}
{
"channel": "market_stats",
"data": {
"markets": [
{
"trading_pair_id": "550e8400-e29b-41d4-a716-446655440000",
"symbol": "BTC/USDC",
"last_trade_price": "64009.0",
"mark_price": "64010.5",
"daily_price_change": "1200.0",
"daily_price_change_percent": "1.91",
"daily_base_token_volume": "842.1",
"daily_quote_token_volume": "53900000.0",
"daily_price_high": "64850.0",
"daily_price_low": "62800.0",
"open_interest": "128.5",
"current_funding_rate": "0.0001",
"funding_timestamp": 1735790400000,
"market_status": "TRADABLE",
"index_price": "64005.2",
"funding_rate": "0.00012",
"premium": "0.00005",
"open_interest_notional": "8225349.25"
}
]
}
}
```
### Instrument
Trading-pair lifecycle and configuration changes, public (no session handshake): new listings, delistings, non-activation edits to tick size, quantity step, order-size bounds, or category, and edits to a perp market's margin ladder. Each perp frame carries the whole ladder as `risk_tiers` (see the payload below). Fee edits do not emit a frame. Every frame also carries the base and quote asset icon URLs, so a client can join a position's `trading_pair_id` to the instrument row and render both icons without a REST call. A **per-market** subscription (`instrument:{trading_pair_id}`) delivers frames for one pair; the bare `instrument` channel (or `instrument:*`) streams every market. A single subscribe-time **`Snapshot`** frame carries the configuration of every listed market as an **array** of per-market baselines (each element a `listing`), and arrives before the live **`Event`** diffs — a raw client must handle the `Snapshot` frame, not only `Event`, to receive that baseline (see the two envelopes below). The snapshot includes inactive (delisted) markets, each flagged `is_active: false`, so positions, orders and history on a delisted market can still resolve its metadata; filter on `is_active` for the tradable set. Markets not yet launched are not included. A per-market snapshot for a delisted market returns that market's row.
```text theme={null}
instrument:{trading_pair_id}
instrument
instrument:*
```
Trading pair ID for a per-market subscription. Omit the segment (or use `*`) to stream every market.
**Example**
```text theme={null}
instrument:550e8400-e29b-41d4-a716-446655440000
```
Detected lag closes the whole connection with 1013; see [Slow-client disconnect](#slow-client-disconnect-close-code-1013). Each live `Event` frame carries one market's **full current configuration**, not just the changed fields, and the subscribe-time `Snapshot` frame carries an array of those same per-market configs (one element per listed market, active or not) — so consume each event, and each element of the snapshot array, as a per-market terminal-state snapshot, never an additive delta; `changed_fields` names which fields the mutation touched, for callers that want to react selectively. Frames are routed by `channel`. Every decimal value is a JSON **string**; keys are snake\_case; the perp-only fields (`min_leverage`, `max_leverage`, `initial_margin_rate`, `maintenance_margin_rate`, `risk_tiers`) are **omitted** (not `null`) for spot markets, which have no leverage or margin parameters. `risk_tiers` is the market's margin ladder, ascending by `tier_level`, with the same fields and values as the REST perp config's `riskTiers`: `tier_level` (a JSON integer), `lower_bound_notional`, `max_leverage`, `initial_margin_ratio`, `maintenance_margin_ratio`, `initial_margin_deduction`, `maintenance_margin_deduction` and `max_position_notional`, with `max_leverage` and `max_position_notional` omitted when the tier has none. `max_leverage`, `initial_margin_rate` and `maintenance_margin_rate` at the top level are tier 1's values. The `base_icon_url` and `quote_icon_url` fields differ: they apply to every market and are always present, carrying an explicit `null` when an asset has no icon configured (never a synthesized URL) rather than being omitted. Every market's frame also carries its asset identity, always present: `base_asset_id` / `quote_asset_id` (UUID strings), `base_token` / `quote_token` (ticker symbols), `base_asset_name` / `quote_asset_name`, and `base_decimals` / `quote_decimals` (JSON integers), named as on the REST `TradingPair`.
`change` is one of `listing` (on a live frame, a market became tradable — created active or re-activated; every snapshot element is a `listing` baseline whatever its `is_active`), `delisting` (deactivated), `halt` / `unhalt` (trading halted / resumed — reserved for a future market-regime producer, not emitted by admin trading-pair edits), or `config_change` (a non-activation edit). A margin-ladder edit that changes any tier, or the position cap that bounds the top tier, emits a `config_change` whose `changed_fields` names `risk_tiers`, plus `max_leverage`, `initial_margin_rate` and `maintenance_margin_rate` when tier 1 changed. Saving an unchanged ladder emits nothing. The payload nests under the channel envelope's `data`, which carries a `trading_pair_id` and a nested `data` object:
```json theme={null}
{
"type": "Event",
"channel": "instrument:550e8400-e29b-41d4-a716-446655440000",
"data": {
"trading_pair_id": "550e8400-e29b-41d4-a716-446655440000",
"data": {
"change": "config_change",
"trading_pair_id": "550e8400-e29b-41d4-a716-446655440000",
"symbol": "BTC/USDC",
"is_active": true,
"tick_size": "0.1",
"quantity_step_size": "0.001",
"min_order_size": "0.001",
"max_order_size": "1000",
"market_type": "margin",
"category": "crypto",
"base_asset_id": "6ba7b810-9dad-11d1-80b4-00c04fd430c8",
"quote_asset_id": "6ba7b811-9dad-11d1-80b4-00c04fd430c8",
"base_token": "BTC",
"quote_token": "USDC",
"base_asset_name": "Bitcoin",
"quote_asset_name": "USD Coin",
"base_decimals": 8,
"quote_decimals": 6,
"base_icon_url": "https://cdn.example/btc.svg",
"quote_icon_url": "https://cdn.example/usdc.svg",
"min_leverage": "1",
"max_leverage": "20",
"initial_margin_rate": "0.05",
"maintenance_margin_rate": "0.03",
"risk_tiers": [
{
"tier_level": 1,
"lower_bound_notional": "0",
"max_leverage": "20",
"initial_margin_ratio": "0.05",
"maintenance_margin_ratio": "0.03",
"initial_margin_deduction": "0",
"maintenance_margin_deduction": "0",
"max_position_notional": "1000000"
},
{
"tier_level": 2,
"lower_bound_notional": "1000000",
"max_leverage": "10",
"initial_margin_ratio": "0.1",
"maintenance_margin_ratio": "0.05",
"initial_margin_deduction": "50000",
"maintenance_margin_deduction": "20000",
"max_position_notional": "5000000"
}
],
"changed_fields": ["tick_size"],
"updated_at": "2026-01-02T03:04:05Z"
}
}
}
```
The subscribe-time baseline arrives as a single **`Snapshot`** frame — `type: "Snapshot"`, with a server-capture `timestamp` — whose `data` is an **array** of per-market `InstrumentEventData` (each with `change: "listing"`), not the per-pair `{ trading_pair_id, data }` wrapper the `Event` frames use. Live changes then follow as the `Event` frames above. Handle both `type` values — a loop that ignores every non-`Event` message never receives the baseline:
```json theme={null}
{
"type": "Snapshot",
"channel": "instrument",
"data": [
{
"change": "listing",
"trading_pair_id": "550e8400-e29b-41d4-a716-446655440000",
"symbol": "BTC/USDC",
"is_active": true,
"tick_size": "0.1",
"quantity_step_size": "0.001",
"min_order_size": "0.001",
"max_order_size": "1000",
"market_type": "margin",
"category": "crypto",
"base_asset_id": "6ba7b810-9dad-11d1-80b4-00c04fd430c8",
"quote_asset_id": "6ba7b811-9dad-11d1-80b4-00c04fd430c8",
"base_token": "BTC",
"quote_token": "USDC",
"base_asset_name": "Bitcoin",
"quote_asset_name": "USD Coin",
"base_decimals": 8,
"quote_decimals": 6,
"base_icon_url": "https://cdn.example/btc.svg",
"quote_icon_url": null,
"min_leverage": "1",
"max_leverage": "20",
"initial_margin_rate": "0.05",
"maintenance_margin_rate": "0.03",
"risk_tiers": [
{
"tier_level": 1,
"lower_bound_notional": "0",
"max_leverage": "20",
"initial_margin_ratio": "0.05",
"maintenance_margin_ratio": "0.03",
"initial_margin_deduction": "0",
"maintenance_margin_deduction": "0",
"max_position_notional": "1000000"
},
{
"tier_level": 2,
"lower_bound_notional": "1000000",
"max_leverage": "10",
"initial_margin_ratio": "0.1",
"maintenance_margin_ratio": "0.05",
"initial_margin_deduction": "50000",
"maintenance_margin_deduction": "20000",
"max_position_notional": "5000000"
}
],
"changed_fields": [],
"updated_at": "2026-01-02T03:04:05Z"
}
],
"timestamp": "2026-01-02T03:04:05Z"
}
```
`@0xmonaco/types` ships the `InstrumentEvent`, `InstrumentEventData`, and `InstrumentChangeKind` types for this channel, and `@0xmonaco/core` exposes the typed [`sdk.ws.instruments(handler, tradingPairId?)`](/sdk/typescript/websockets#instruments-channel) subscription, which parses the snake\_case wire keys onto those types and fans the `Snapshot` baseline out as one event per market. A raw WebSocket client reads the snake\_case wire keys directly, or maps them onto `InstrumentEventData` itself (`trading_pair_id` → `tradingPairId`, `is_active` → `isActive`, `tick_size` → `tickSize`, `base_icon_url` → `baseIconUrl`, `quote_icon_url` → `quoteIconUrl`, `base_asset_id` → `baseAssetId`, `base_decimals` → `baseDecimals`, `min_leverage` → `minLeverage`, `risk_tiers` → `riskTiers` (and each tier's keys likewise, `tier_level` → `tierLevel`), `changed_fields` → `changedFields`, and so on; that type describes the ergonomic shape, not these raw keys).
### Orders
Order lifecycle events for the authenticated user, including placement, fills, cancellations, rejections, and expirations.
```text theme={null}
orders
orders:{trading_pair_id}:{mode}
```
Trading pair filter. Omit it by using `orders` for all of the authenticated user's orders.
Trading mode filter. Use `orders:{trading_pair_id}:*` to filter by pair across all modes.
**Example**
```text theme={null}
orders
orders:550e8400-e29b-41d4-a716-446655440000:SPOT
```
### Balances
Balance state changes for the authenticated user.
```text theme={null}
balances
```
Emits the authenticated user's balance state changes, including available, locked, total, reason, and reference fields.
### Movements
Ledger-style balance movement entries for the authenticated user.
```text theme={null}
movements
```
Emits entries for deposits (spot and margin-routed), withdrawals, funding settlements, and the balance a cancelled or expired order unlocks. Fills and fees are **not** emitted here: a fill's ledger entries are written durably and read back through movement history (`GET /api/v1/accounts/movements`), while its live signal is a `balance_update` on the [`balances`](#balances) channel — a versioned spot row for a spot-owned leg, and for a margin fill an unversioned frame carrying the margin account's post-fill collateral.
### Positions
Margin position changes, for cross and isolated risk buckets alike, including size, status, mark price, PnL, and liquidation fields.
```text theme={null}
positions
positions:{trading_pair_id}
```
Optional trading pair filter. Omit it for all positions owned by the authenticated user.
**Example**
```text theme={null}
positions:550e8400-e29b-41d4-a716-446655440000
```
A fill sends a live `OPEN` frame for every open position in each risk bucket it touched, taker and makers alike: the bucket of the position each side's order is bound to. In a cross risk bucket that is every position of the bucket, in any pair, because a trade in one pair moves the liquidation price of the others; an isolated risk bucket holds a single position. Positions in the account's other risk buckets are not re-sent, even in the same pair. Each position is sent at most once per fill, and every frame carries the fill's `version`. Each frame names its own `trading_pair_id`, so `positions:{trading_pair_id}` receives the frames for positions in that pair even when the trade was in another one. Liquidation slices and auto-deleveraging refresh the liquidated account's risk bucket and each counterparty's risk bucket the same way.
Collateral transfers use `margin_added` or `margin_reduced`. A direct isolated-position margin add or reduction sends that position with the transfer's version. Adding margin to a copy position sends the isolated child plus every open position in its copy cross risk bucket, also versioned. Cross-risk-bucket `ALLOCATE`/`RELEASE`, copy-follow creation or allocation growth, and copy-stop return refresh every open position in the affected cross risk bucket without a version because those commands do not write position rows. Wallet `IN`/`OUT`, plain isolated-risk-bucket allocation without a position collateral delta, and empty scopes send no position frame. Successful frames use the command's captured post-command state and are released only after its durable acknowledgement.
Where enabled, the channel also carries `position_valuation` frames: about every five seconds, each open position's `mark_price`, `unrealized_pnl`, `liquidation_price` (`null` when none can be derived), `initial_margin_required` and `maintenance_margin_required`, with `valued_at`, the time the server read it. A frame is sent when any of the five changed, and otherwise periodically, normally at least once a minute. It carries no size, status or version: apply it only to a position you hold as `OPEN` whose `updated_at` is earlier than the frame's `valued_at` (a valuation read before the position's last mutation is stale), ignore it for an unknown or terminal one, and never close a position from one. See [Position Valuation Frames](/sdk/typescript/websockets#position-valuation-frames).
Non-zero funding payments emit ranked `OPEN` frames from state captured inside the funding command; zero payments emit no position frame. Full closes, liquidation victims/backstop exits and ADL closes normally emit ranked terminal frames with lifetime reduced quantity — the sum of every reducing execution — and the exact terminal classification: ordinary closes and flattened ADL counterparties are `CLOSED`, while liquidation exits and the bankrupt ADL side are `LIQUIDATED`. After restart, a terminal frame rebuilt from pre-upgrade reduction history omits `version` when that history lacks the size or margin fields needed to reconstruct complete lifetime values; incomplete size history carries only the known partial total. Reconcile that frame by full field comparison. Detect terminal state from `status`, never from `size === "0"`.
### Conditional Orders
Take-profit, stop-loss and trailing-stop lifecycle changes for the authenticated
user. A trailing stop additionally emits `reason: "armed"` when tracking begins
and `reason: "ratcheted"` on every trigger move, each frame carrying the new
`triggerPrice` and `watermarkPrice` (see [Trailing Stop](/developers/trade#trailing-stop)).
```text theme={null}
conditional_orders
conditional_orders:{trading_pair_id}
```
Optional trading pair filter. `conditional-orders` with a hyphen is also accepted.
**Example**
```text theme={null}
conditional_orders:550e8400-e29b-41d4-a716-446655440000
```
### TWAP Orders
Owner-scoped TWAP parent-order updates. Every mutation emits one
`twap_order_update` carrying the full parent snapshot (configuration plus
progress: executed quantity/notional, average fill price, slice counters,
next-slice cursor, state, `execution_style`, the conditional-trigger fields,
and any terminal reason such as end-of-window shortfall or `trigger expired`).
The `reason` field names the mutation: `created`, `slice_placed`,
`slice_skipped`, `cancelled`, or `completed` for every parent;
`passive_child_placed`, `child_repriced`, and `passive_fill` for a `PASSIVE`
parent working a resting post-only child (its deadline sweeps and crossing
takes still arrive as `slice_placed`); and `triggered` or `trigger_expired` for
a conditional parent. Two of the passive reasons are looser than they read:
`child_repriced` is also emitted by the cancel-only fallback, with no child
left resting, and the maker fill that completes a parent reports `completed`
rather than `passive_fill`. Only the authenticated owner's parents are ever
delivered. See [TWAP orders](/trading-mechanics/twap-orders) for the execution
mechanics, including [passive execution](/trading-mechanics/twap-orders#passive-execution).
On subscribe, before any live frame, the server sends a `Snapshot` frame on the
`twap_orders` channel whose `data` is an array with one `TwapOrderEventData` row
per owned **non-terminal** parent — `PENDING` (awaiting its start window, or, for
a conditional parent, still awaiting its price trigger) or `ACTIVE` (triggered
and slicing) — honouring the optional pair filter. Each row is the same shape as a
live `twap_order_update` `data`, with `reason: "snapshot"`. An owner with no live
parents receives an empty array, not no frame. Terminal parents (`COMPLETED` /
`CANCELLED`) are excluded as history — read those from the REST TWAP order
listing. The baseline reflects **persisted** state, so a parent that reached a
terminal state just before the subscription opened can still appear
`ACTIVE`/`PENDING` until that transition is persisted, and its terminal event —
sent before the subscription existed — does not re-fire; consume the snapshot as
a seed to merge and reconcile against the REST read, not as an authoritative
replacement set. A repository read failure returns `SNAPSHOT_UNAVAILABLE` and
leaves the subscription active (live diffs still flow); treat it as a transient
resync signal, not a failure.
Live updates and snapshot rows can carry an optional `version`, derived from the
sequencer step that wrote that parent. Reconcile only rows with the same
`twap_order_id`: accept `>=`, reject lower versions, and let the snapshot win an
equal-version tie. Gaps are normal. An absent version means unknown legacy
state, not zero, and versions from different parents are unrelated.
```text theme={null}
twap_orders
twap_orders:{trading_pair_id}
```
Optional trading pair filter. Omit it for all TWAP parents owned by the authenticated user.
**Example**
```text theme={null}
twap_orders:550e8400-e29b-41d4-a716-446655440000
```
### Liquidations
Liquidation-risk alerts and liquidation status changes for the authenticated user.
```text theme={null}
liquidations
liquidations:{trading_pair_id}
```
Optional trading pair filter for liquidation alerts owned by the authenticated user.
**Example**
```text theme={null}
liquidations:550e8400-e29b-41d4-a716-446655440000
```
#### Subscribe-time snapshot
On subscribe, before any live alert, the server sends a `Snapshot` frame on the
bare `liquidations` channel whose `data` is an array with one
`LiquidationEventData` row per **in-flight** liquidation record the authenticated
user owns — one the matching engine is still actively liquidating (`status:
"UNRESOLVED"`). These are exactly the records the live channel keeps reconciling:
the engine emits a `liquidation_alert` on each transition, including the terminal
one. A `COMPLETED` record, and a `BAD_DEBT` record (an insolvent account whose
insurance resolution is a settlement-side process the live channel does not
report), are excluded. Each row is the same shape as a live `liquidation_alert`
`data`. An owner with no in-flight liquidation receives an empty array, not no
frame.
A liquidation record is account-scoped and carries no trading pair, so a snapshot
row's `position_id`, `trading_pair_id`, `liquidation_price`, and `mark_price` are
always `null` — the live alert fills those from the live position the snapshot
lacks. `risk_bucket_state` is `Liquidating` on every row: an in-flight record is a
risk bucket under active liquidation, and the record does not persist the exact
live bucket state, so the next live alert carries the precise value. Because a snapshot
row cannot be attributed to a pair, a pair-scoped
`liquidations:{trading_pair_id}` subscription baselines with an **empty** array;
subscribe to the bare `liquidations` channel for the account-wide baseline.
The baseline reflects **persisted** state, so consume it as a seed to merge and
reconcile against a REST read, not as an authoritative replacement set: a record
that terminalized just before the subscription opened can still appear
non-terminal until that transition is persisted, and its terminal alert — sent
before the subscription existed — does not re-fire. A repository read failure
returns `SNAPSHOT_UNAVAILABLE` and leaves the subscription active (live alerts
still flow); treat it as a transient resync signal, not a failure.
#### Recovered episodes
A liquidation episode that recovers ends with a terminal `liquidation_alert`
too, not only one that liquidates to completion. When a breached risk bucket
heals back over its maintenance requirement — on a liquidation step, or with no
step at all on an oracle mark, a funding settlement, or an auto-deleveraging
close that lifts a counterparty's bucket — the episode's record closes as
`status: "COMPLETED"` and the owner receives one alert with that status and
`risk_bucket_state: "Normal"`, carrying the same `liquidation_id` as the earlier
alerts for the episode. The alert describes the recovered bucket's own
position, so a pair-scoped `liquidations:{trading_pair_id}` subscription on that
bucket's pair receives it; a bucket with no open position carries `null`
position fields. Treat it as the episode's terminal frame: it supersedes any
in-flight (`UNRESOLVED`) row a subscribe-time snapshot seeded.
### Copy
Copy trading events for the authenticated **follower**: every change to one of your copy relationships, and every leader fill a relationship could not mirror. There is no per-pair variant. The leader of a relationship receives nothing on this channel.
```text theme={null}
copy
```
Two event types share the channel, named by the frame's `event_type`:
* `copy_relationship_update`: the relationship's full current state after a change. `change` says why the frame was sent: `created`, `updated`, `close_only`, `stopped`, `skip_recorded`, or `reactivated`. `status` is `ACTIVE`, `CLOSE_ONLY`, or `STOPPED`, and `STOPPED` is terminal.
* `copy_skip`: one leader fill the relationship did not mirror. `reason` is `INSUFFICIENT_MARGIN`, `BELOW_MIN`, `LEVERAGE_MISMATCH`, `MARKET_STATE`, `LEADER_EQUITY`, or `NO_LIQUIDITY`. Only `INSUFFICIENT_MARGIN` advances `consecutive_skips`.
Both payloads name the leader by `leader_wallet`, their canonical lowercase wallet address; the channel never carries an internal leader id. `version` is the durable-log sequence of the step that produced the frame. For one relationship, a higher version is newer. `cum_net_realized` and `hwm` are the relationship's realized PnL net of fees since `started_at` and its high-water mark, which is the basis for the leader's profit share. They are **not** the balance of the copy risk bucket.
**Subscribe snapshot.** The snapshot is an array of your `ACTIVE` and `CLOSE_ONLY` relationships in the authenticated application. Each row has the `copy_relationship_update` payload shape with `change: "snapshot"`. Skips are events, not state, so the snapshot carries none. Payout events are planned for a later release and are not sent today.
Every decimal value is a JSON string, and wire keys are **snake\_case**:
```json theme={null}
{
"channel": "copy",
"data": {
"event_type": "copy_skip",
"user_id": "22222222-2222-2222-2222-222222222222",
"data": {
"relationship_id": "33333333-3333-3333-3333-333333333333",
"leader_wallet": "0x2c7536e3605d9c16a7a3d7b1898e529396a65c23",
"leader_trade_id": "66666666-6666-6666-6666-666666666666",
"leader_order_id": "77777777-7777-7777-7777-777777777777",
"trading_pair_id": "11111111-1111-1111-1111-111111111111",
"reason": "INSUFFICIENT_MARGIN",
"requested_quantity": "0.5",
"consecutive_skips": 3,
"version": 918343
}
}
}
```
### Account
Margin-account health for the authenticated user: equity, collateral, free collateral, margin ratio, and maintenance requirement. There is no per-pair variant — one subscription covers every margin account the user owns. Frames flow while an account is **active** (it holds an open position or a resting order reserve): when its last position and reserve clear, one final frame carries the released state (maintenance requirement back to zero, free collateral restored), and after that the account sends no keepalive while it stays flat. A flat account still gets a frame after its collateral moves: a transfer between the spot wallet and margin, an on-chain deposit into margin or a withdrawal from it, a transfer between your margin accounts, closing a sub-account, a copy-trading allocation, or an insurance payout. Moves that land before the producer's next tick collapse into one frame carrying the latest state, and that frame normally arrives on the next tick, within \~2s of the move. The producer reads at most 512 flat accounts per tick, so a burst wider than that spreads over the following ticks. The frame carries the account's current collateral on the same per-account `sequence` as every other frame, so the usual ordering rule applies. On-chain deposits and withdrawals also appear on the [`movements`](#movements) channel. The producer samples on a \~2s tick, so a position or reserve that opens and fully closes inside one tick is below the sampling cadence — no frame marks it, and the account's previous state stands until its next activity.
The server-side producer that emits live `account_update` frames runs on every deployment, mainnet included. Enablement is a deployment switch rather than a delivery guarantee, so confirm you are receiving frames before depending on the live stream. The subscribe-time snapshot below is served by the WebSocket API rather than that producer, so it is unaffected by the switch.
```text theme={null}
account
```
Detected lag closes the whole connection with 1013; see [Slow-client disconnect](#slow-client-disconnect-close-code-1013). A frame is published when the account's health changes **and** republished unchanged as a keepalive after a bounded quiet interval (\~30s), so treat a repeat as a keepalive, not a state transition. Each frame carries the account's full current health, so consume it as a terminal-state snapshot per margin account, not an additive delta. Dedupe LIVE frames per account by `updated_at`, newest wins; `sequence` (a plain JSON number) only breaks ties between frames carrying the same `updated_at` (those come from the same producer run). A snapshot row is not ranked this way — any live frame supersedes it. `sequence` resets to 0 on a producer restart (which can happen while your socket stays connected), may skip values, and is never a gap detector or a cross-restart order. A subscription made while an account is flat receives no LIVE frame for it until the account re-activates, because keepalives cover only active accounts — but the subscribe-time snapshot below includes it, so the baseline is complete. Every decimal value is a JSON string; wire keys are **snake\_case** (like every other raw frame on this page), and the payload nests under the channel envelope's `data`:
```json theme={null}
{
"channel": "account",
"data": {
"event_type": "account_update",
"user_id": "550e8400-e29b-41d4-a716-446655440000",
"data": {
"margin_account_id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"sequence": 42,
"equity": "10250.75",
"collateral": "10000.00",
"free_collateral": "8420.10",
"margin_ratio": "0.12",
"maintenance_requirement": "1230.09",
"updated_at": "2026-08-25T12:00:00Z"
}
}
}
```
#### Subscribe-time snapshot
On subscribe, before any live frame, the server sends a `Snapshot` frame on the `account` channel whose `data` is an array with one row per margin account the authenticated user owns **in the session's application** — **including flat accounts**, which the live producer emits for only when their collateral moves. Each row's `updated_at` is the instant that account's state was **persisted**, not the time the snapshot was read, so rows of one frame legitimately differ.
Live delivery is per user and carries no application filter, so a wallet trading through more than one application also receives live frames for its other accounts. Those are terminal-state payloads, so such an account baselines itself from its next frame (within the \~30s keepalive) despite not appearing in this list. Each row carries that account's current persisted health. An account that is flat **and otherwise empty** — no positions and no open risk bucket — reports its deposited collateral as both `equity` and `free_collateral`, with `maintenance_requirement` and `margin_ratio` at `0`. An account that is flat but still holds an open risk bucket keeps that bucket's allocated collateral and unsettled realized PnL, so its `equity` need not equal its `collateral`. A user who owns no margin account receives `"data": []`, not a missing frame.
A snapshot row is the live `data` payload with **`sequence` omitted**:
```json theme={null}
{
"type": "Snapshot",
"channel": "account",
"data": [
{
"margin_account_id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"equity": "10000.00",
"collateral": "10000.00",
"free_collateral": "10000.00",
"margin_ratio": "0",
"maintenance_requirement": "0",
"updated_at": "2026-08-25T12:00:00Z"
}
],
"timestamp": "2026-08-25T12:00:01Z"
}
```
`sequence` is the live producer's process-local per-account counter and the WebSocket API is a different service, which cannot mint one; `0` is not available either, because it is indistinguishable from a restarted producer's first frame and would make a consumer applying the dedupe rule discard real live frames. Absent means unknown — never read it as zero. Ranking still works without it, but **not by comparing a snapshot row's `updated_at` against a live frame's**. A live frame always supersedes a snapshot row for the same margin account, whatever the timestamps say: the snapshot is stamped when its state was *persisted* (database write time) and a live frame when the engine *sampled* it, so under persistence lag a later-written row can carry older state. Use `updated_at` to order live frames against each other — newest wins — with `sequence` breaking ties inside one producer run, which a snapshot is not part of. A durable per-account version would make the two surfaces genuinely comparable; until then the snapshot is a seed, not an authority. That rule also applies to live frames emitted *before* your subscription activated: the connection buffers them while the snapshot read runs and delivers them after it, so one can supersede a newer snapshot row. A later frame normally corrects this within the \~30s keepalive. The exception is an account that went flat during that window and whose final frame never reached you — the producer emits one, but it derives departures from in-process state, so a producer restart across the transition emits none — in which case you keep a stale active account until you reconnect and re-seed. Reconnecting and re-seeding is the recovery for this, as it is for [dropped events](#slow-client-disconnect-close-code-1013).
`margin_ratio` is the maintenance margin required divided by equity; it is `0` for an account with no open positions. It saturates to a maximum-distress sentinel (a very large decimal string, `Decimal::MAX` \~7.9e28) whenever the ratio cannot be computed or represented: a maintenance requirement still owed at zero or negative equity, and equally at near-zero positive (dust) equity where the true ratio exceeds the decimal range. Treat a value at or above the liquidation threshold as maximal distress and clamp it for display. `equity` is mark-inclusive (collateral + unrealized PnL + funding). The typed `sdk.ws.account(handler)` helper (see [TypeScript WebSockets](/sdk/typescript/websockets#account-channel)) subscribes and hands you the camelCased `AccountEvent` shape. To consume the channel over the raw WebSocket protocol above instead, send a `Subscribe` frame naming the `account` channel and read the snake\_case wire keys directly — or map them yourself onto the `AccountEvent` type in `@0xmonaco/types` (`event_type` → `eventType`, `margin_account_id` → `marginAccountId`, `free_collateral` → `freeCollateral`, and so on; that type describes the ergonomic shape the helper exposes, not these raw keys).
# V0.1.1
Source: https://docs.0xmonaco.com/releases/v0.1.1
# Monaco Protocol SDK v0.1.1
## Initial Release
The first stable release of the Monaco Protocol SDK, providing developers with essential tools to integrate with the Monaco Protocol CLOB.
### Core Features
* **Complete TypeScript SDK**: Full TypeScript SDK for Monaco Protocol integration
* **Authentication System**: Frontend and backend authentication support
* **Deposit & Withdrawal**: Complete deposit and withdrawal functionality
* **Trading Operations**: Order placement, management, and cancellation
* **React Integration**: React hooks and components for seamless frontend integration
* **Comprehensive API Coverage**: Full API coverage for all Monaco Protocol features
### Package Architecture
* **`@0xmonaco/core`** - Core SDK functionality including API clients and trading operations
* **`@0xmonaco/contracts`** - Smart contract interfaces and ABIs
* **`@0xmonaco/types`** - TypeScript type definitions for all SDK components
* **`@0xmonaco/react`** - React hooks and components for frontend integration
* **`@0xmonaco/utils`** - Utility functions and helpers
### Getting Started
```bash theme={null}
npm install @0xmonaco/core
```
### Key Capabilities
**Authentication**
* Wallet-based authentication flow
* Backend service authentication
* Session management and token refresh
**Trading**
* Place limit and market orders
* Cancel and replace existing orders
* Real-time order status updates
* Order book access
**Vault Operations**
* Deposit tokens to the protocol
* Withdraw funds
* Balance management
* Sub-account support
**React Integration**
* Pre-built hooks for common operations
* Type-safe components
* State management helpers
### Technical Foundation
This initial release establishes the foundation for building sophisticated trading applications on the Monaco Protocol. The SDK provides a clean, typed interface for all protocol operations while maintaining flexibility for custom implementations.
# V0.1.5
Source: https://docs.0xmonaco.com/releases/v0.1.5
# Monaco Protocol SDK v0.1.5
## General Release Update
Stable release with improved reliability and performance optimizations across all packages.
### What's Improved
* **Enhanced Stability**: Improved reliability across the entire SDK
* **Performance Optimizations**: Various performance improvements throughout the codebase
* **Bug Fixes**: Multiple reliability improvements and bug fixes
* **Package Consistency**: Updated all packages to maintain version consistency
### Technical Details
This release focuses on stability and reliability improvements:
* General performance optimizations across all packages
* Enhanced error handling and edge case management
* Improved package interdependencies
* Better test coverage and reliability
### Packages Updated
* `@0xmonaco/core@0.1.5`
* `@0xmonaco/contracts@0.1.5`
* `@0xmonaco/types@0.1.5`
* `@0xmonaco/react@0.1.5`
* `@0xmonaco/utils@0.1.5`
### Installation
```bash theme={null}
npm install @0xmonaco/core@0.1.5
```
### Notes
This is a maintenance release focused on stability and performance. All existing APIs remain unchanged, making this a safe upgrade for all users.
# V0.1.6
Source: https://docs.0xmonaco.com/releases/v0.1.6
# Monaco Protocol SDK v0.1.6
## Multi-Vault Architecture Support
The SDK now supports multiple vault contracts to align with the new backend architecture. Each client ID (app) now uses its own dedicated contract, providing better isolation and scalability.
### What's New
* **Multiple Vault Contract Support**: Added support for multiple vault contracts to align with the new backend architecture
* **Enhanced Contract Isolation**: Each client ID (app) now uses its own dedicated contract
* **Improved Scalability**: Better isolation and scalability for applications
* **React Package Addition**: New React package with hooks and components
### Technical Details
This release introduces significant architectural improvements:
* Support for multiple vault contracts per backend deployment
* Enhanced contract isolation for better security
* Improved application-specific contract management
* New React integration package
### Packages Updated
* `@0xmonaco/core@0.1.6`
* `@0xmonaco/contracts@0.1.6`
* `@0xmonaco/types@0.1.6`
* `@0xmonaco/react@0.1.6` (New!)
* `@0xmonaco/utils@0.1.5` (dependency)
* `@0xmonaco/mcp-server@0.1.5` (tooling)
### Installation
```bash theme={null}
npm install @0xmonaco/core@0.1.6
npm install @0xmonaco/react@0.1.6 # For React applications
```
### Migration Notes
If you're upgrading from a previous version, the multi-vault architecture is backward compatible. No breaking changes are required for existing integrations.
# V0.2.0
Source: https://docs.0xmonaco.com/releases/v0.2.0
# Monaco Protocol SDK v0.2.0
## Trading Enhancements & Real-time Data
This release introduces significant trading improvements including market order slippage protection, advanced limit order features, and real-time order event streaming via WebSocket.
### What's New
**WebSocket Real-time Data Streaming**
* **Live Order Updates**: Real-time streaming of order status changes with event types (OrderPlaced, OrderMatched, OrderPartiallyFilled, OrderFilled, OrderCancelled, OrderRejected, OrderExpired)
* **OHLCV Data Streaming**: Real-time candlestick data across 6 timeframes (1m, 5m, 15m, 1h, 4h, 1d)
* **Automatic Reconnection**: Built-in exponential backoff and connection health monitoring
* **Multiple Subscriptions**: Subscribe to events for specific trading pairs and intervals simultaneously
**Market Order Slippage Protection**
* **Slippage Tolerance**: Set maximum acceptable price deviation (0.1% to 10%)
* **Automatic Price Protection**: Orders reject if slippage exceeds specified tolerance
* **Enhanced Order Response**: Added `actual_slippage_bps` and `max_slippage_bps` to match results
* **Backend Validation**: Server-side slippage validation with configurable limits
**Limit Order Enhancements**
* **Good Till Cancel (GTC)**: Automatic 90-day default expiration for limit orders
* **Custom Expiration Dates**: Set specific expiration dates for limit orders
### Technical Details
**WebSocket Real-time Data**
Subscribe to real-time order events:
```typescript theme={null}
// Connect and subscribe to order events
await sdk.websocket.orders.connect();
sdk.websocket.orders.subscribeToOrderEvents('ETH-USD', 'SPOT', (event) => {
console.log('Event:', event.eventType, 'Order:', event.orderId);
});
```
Subscribe to real-time OHLCV candlestick data:
```typescript theme={null}
// Connect and subscribe to 1-minute candles
await sdk.websocket.ohlcv.connect();
sdk.websocket.ohlcv.subscribeToOHLCV('ETH-USD', 'SPOT', '1m', (event) => {
console.log('Price:', event.candlestick.c, 'Volume:', event.candlestick.v);
});
```
WebSocket features automatic reconnection, heartbeat monitoring, and JWT authentication. See [WebSocket documentation](/websocket) and [OHLCV documentation](/ohlcv) for complete details.
**Market Orders with Slippage Protection**
Market orders now accept an optional `slippageTolerance` parameter:
```typescript theme={null}
const order = await sdk.trading.placeMarketOrder(
'ETH-USD',
'BUY',
'1.0',
{ slippageTolerance: 0.01 } // 1% max slippage
);
```
The SDK converts decimal tolerance to basis points and sends to backend. Orders are rejected with "Slippage tolerance exceeded" error if insufficient liquidity exists within bounds.
**Limit Orders with GTC and Custom Expiration**
Limit orders now support custom expiration dates:
```typescript theme={null}
const order = await sdk.trading.placeLimitOrder(
'ETH-USD',
'BUY',
'1.0',
'2500.00',
{
expirationDate: '2025-12-31T23:59:59Z' // Custom expiration
}
);
```
Orders default to 90-day expiration if no `expirationDate` is provided.
### Packages Updated
* `@0xmonaco/core@0.2.0`
* `@0xmonaco/types@0.2.0`
* `@0xmonaco/react@0.2.0`
* `@0xmonaco/contracts@0.1.6`
* `@0xmonaco/utils@0.1.5`
### Installation
```bash theme={null}
npm install @0xmonaco/core@0.2.0
npm install @0xmonaco/react@0.2.0 # For React applications
```
### Migration Notes
This is a backward-compatible release. Existing orders continue to work unchanged:
* Market orders without `slippageTolerance` work as before, but we recommend adding slippage protection for volatile markets
* Limit orders automatically receive 90-day GTC expiration if no `expirationDate` is specified
* WebSocket is an optional feature - existing REST API usage remains unchanged
### Documentation
* [WebSocket API](/websocket) - Real-time order events streaming
* [OHLCV Data](/ohlcv) - Real-time candlestick data streaming
* [Order Management](/developers/trade) - Market, limit, cancel, and replace order documentation
# V0.3.0
Source: https://docs.0xmonaco.com/releases/v0.3.0
# Monaco Protocol v0.3.0
## Orderbooks and Flat Fees
This release introduces major improvements to orderbook functionality, fee management, and order lifecycle handling. Key highlights include real-time orderbook snapshots via WebSocket, flat fee support, and comprehensive time-in-force options.
### What's New
**Orderbook Enhancements**
* **Real-time Orderbook Snapshots**: Full orderbook state via WebSocket with initial snapshot on subscription
* **Orderbook Magnitude Grouping**: Optional price level aggregation for cleaner visualization (0.0001 to 10000 increments)
* **Improved Orderbook Endpoint**: Enhanced REST API with better performance and data structure
**Fee System Improvements**
* **Flat Fee Support**: New fee calculation method alongside existing basis point fees
* **Fee HTTP Endpoints**: Dedicated API endpoints for fee management and queries
* **Pre-settlement Fee Calculation**: Fees calculated before settlement for accurate balance checks
**Time-in-Force Support**
* **Full TIF Implementation**: Support for GTC (Good-Till-Cancel), IOC (Immediate-or-Cancel), and FOK (Fill-or-Kill)
* **Order Rejection Events**: New event types for rejected orders with detailed reasons
* **Enhanced Order Validation**: Pre-validation for FOK orders to prevent partial fills
**Order Management Improvements**
* **Order Replacement**: Renamed "Update Order" to "Replace Order" for clarity
* **Order Quantity Validation**: Enhanced validation on order replacement
* **Consistent Enum Formatting**: Uppercase enums across entire codebase (SPOT, MARGIN, BUY, SELL, etc.)
* **Side and Role Fields**: Added to OrderPlaced and OrderPartiallyFilled WebSocket events
### Technical Details
**WebSocket Connection Methods**
Connect to WebSockets using either auto-connect during login or explicit connection:
```typescript theme={null}
// Method 1: Auto-connect authenticated WebSockets during login
const authState = await sdk.login(clientId, { connectWebSocket: true });
// Orders WebSocket is now connected and ready
// Method 2: Explicit connection after login
const authState = await sdk.login(clientId);
await sdk.websocket.orders.connect();
// Public WebSockets (orderbook, OHLCV) don't require authentication
await sdk.websocket.orderbook.connect();
await sdk.websocket.ohlcv.connect();
```
**Orderbook WebSocket with Magnitude**
Subscribe to orderbooks with optional price grouping:
```typescript theme={null}
// Full precision orderbook
sdk.websocket.orderbook.subscribeToOrderbookEvents(
'BTC/USDC',
'SPOT',
(update) => {
console.log('Bids:', update.bids, 'Asks:', update.asks);
}
);
// Grouped by $10 increments
sdk.websocket.orderbook.subscribeToOrderbookEvents(
'BTC/USDC',
'SPOT',
(update) => {
console.log('Grouped orderbook:', update);
},
10 // Magnitude: group by $10
);
```
**Time-in-Force Orders**
```typescript theme={null}
// Immediate-or-Cancel
await sdk.trading.placeLimitOrder(
pairId,
'BUY',
'1.0',
'2500.00',
{ timeInForce: 'IOC' }
);
// Fill-or-Kill
await sdk.trading.placeLimitOrder(
pairId,
'BUY',
'1.0',
'2500.00',
{ timeInForce: 'FOK' }
);
```
### Architecture & Infrastructure
**Admin API**
* New admin endpoints for system management and monitoring
* Procedural macro-based authentication decorators replacing middleware
**Authentication Improvements**
* Unified auth extractor with procedural macros
* Replace middleware with `#[require_auth]`, `#[require_master]`, and `#[require_permission]` decorators
* Cleaner, more maintainable authentication layer
**Performance Optimizations**
* Reduced lock contention during API initialization and order expiration
* Trading pair cache updates for faster lookups
* Improved matching engine transaction handling
**Testing & Code Quality**
* Split order E2E tests into multiple focused test suites
* Resolved foreign key constraint violations in indexer tests
* Enhanced test coverage with `.env` file support
* Fixed IOC order event emission for partial fills
### Bug Fixes
* Fixed double serialization issues with consistent uppercase enum representation
* Corrected BPS calculation in matching engine for order creation
* Fixed maker order status and balance updates when trades occur
* Resolved insufficient balance errors on order creation
* Fixed IOC/FOK partial fill locked balance issues
* Prevented duplicate blockchain events with enhanced duplicate checking
* Fixed missing cancelled events for IOC orders
* Improved order settlement status checks before cancellation
### Documentation
* Auto-generated API documentation from OpenAPI specs
* Updated SDK documentation for latest features
* Added cloud infrastructure architecture documentation
* Comprehensive orderbook WebSocket examples
* Enhanced trading pair and order object documentation
### Packages Updated
* `@0xmonaco/core@0.3.0`
* `@0xmonaco/types@0.3.0`
* `@0xmonaco/react@0.3.0`
### Installation
```bash theme={null}
npm install @0xmonaco/core@0.3.0
npm install @0xmonaco/react@0.3.0 # For React applications
```
### Migration Notes
**Breaking Changes:**
* Enum values are now consistently uppercase (SPOT, MARGIN, BUY, SELL, etc.)
* Update any hardcoded lowercase enum strings to uppercase
**New Features:**
* Orderbook magnitude parameter is optional (defaults to full precision)
* Time-in-force defaults to GTC if not specified
### Contributors
* @kamwithak
* @codebycarson
* @ogarciarevett
* @dssei
* @ppitlehra
* @Kbhat1
### Full Changelog
This release merged 55 pull requests.
# V0.3.1
Source: https://docs.0xmonaco.com/releases/v0.3.1
# Monaco Protocol SDK v0.3.1
## Balance Management Enhancements
This release adds new SDK methods for querying user token balances, building on the v0.3.0 foundation. These additions provide more flexible and efficient ways to access account balance information.
### What's New
**Balance Query Methods**
* **Paginated Balance Listing**: New `getUserBalances()` method with pagination support
* **Individual Token Balance**: New `getUserBalanceByToken()` method for single-token queries
* **Enhanced Balance Details**: Returns available, locked, and total balance for each token
### Technical Details
**Get All User Balances (Paginated)**
Query all token balances for the authenticated user with pagination:
```typescript theme={null}
// Get first 20 balances
const response = await sdk.profile.getUserBalances({
limit: 20,
offset: 0
});
console.log('Total balances:', response.total);
response.balances.forEach(balance => {
console.log(`${balance.symbol}: ${balance.total_balance}`);
});
```
**Get Balance for Specific Token**
Query balance for a single token address:
```typescript theme={null}
// Get balance for a specific token
const balance = await sdk.profile.getUserBalanceByToken(
'0x23e459c24c4925ba89205c400dceca7abc11723d'
);
console.log(`${balance.symbol}:`, {
available: balance.available_balance,
locked: balance.locked_balance,
total: balance.total_balance
});
```
**Response Structure**
Both methods return detailed balance information:
```typescript theme={null}
interface AccountBalance {
token: string; // Token address (0x format)
symbol: string; // Token symbol (e.g., 'USDC')
decimals: number; // Token decimal places
available_balance: string; // Available for trading
locked_balance: string; // Locked in active orders
total_balance: string; // Total (available + locked)
}
```
### Use Cases
**Portfolio Dashboard**
```typescript theme={null}
// Display all token holdings
const { balances } = await sdk.profile.getUserBalances({ limit: 100 });
const portfolio = balances.map(b => ({
asset: b.symbol,
value: parseFloat(b.total_balance),
inOrders: parseFloat(b.locked_balance)
}));
```
**Pre-Trade Balance Check**
```typescript theme={null}
// Check if user has sufficient balance before placing order
const usdcBalance = await sdk.profile.getUserBalanceByToken(usdcAddress);
if (parseFloat(usdcBalance.available_balance) >= orderAmount) {
await sdk.trading.placeMarketOrder(/* ... */);
}
```
**Infinite Scroll Balance List**
```typescript theme={null}
// Load balances in chunks for UI
let offset = 0;
const limit = 20;
let hasMore = true;
while (hasMore) {
const response = await sdk.profile.getUserBalances({ limit, offset });
displayBalances(response.balances);
offset += limit;
hasMore = offset < response.total;
}
```
### Packages Updated
* `@0xmonaco/core@0.3.1`
* `@0xmonaco/types@0.3.1`
* `@0xmonaco/react@0.3.1`
### Installation
```bash theme={null}
npm install @0xmonaco/core@0.3.1
npm install @0xmonaco/react@0.3.1 # For React applications
```
### Migration Notes
This is a backward-compatible release. No breaking changes from v0.3.0:
* All v0.3.0 features continue to work unchanged
* New balance methods are additive - existing `getProfile()` method still works
* Existing code requires no modifications to upgrade
### Documentation
* [Profile SDK Methods](/sdk/typescript/profile) - Complete profile and balance documentation
* [v0.3.0 Release Notes](/releases/v0.3.0) - Previous release with orderbook and fee enhancements
### Related Backend Changes
This SDK release corresponds to backend API enhancements:
* New `GET /api/v1/accounts/balances` endpoint (paginated)
* New `GET /api/v1/accounts/balances/:token` endpoint (individual token)
* Performance optimizations for balance queries
# V0.4.0
Source: https://docs.0xmonaco.com/releases/v0.4.0
# Monaco Protocol SDK v0.4.0
## Custom RPC Configuration & Network Flexibility
This release introduces configurable RPC endpoints and custom network support, giving developers full control over their infrastructure and network connectivity.
### What's New
**Required RPC Configuration**
* **Custom RPC Endpoint**: `seiRpcUrl` is now a required parameter when initializing the SDK
* **Rate Limit Control**: Each application can now use their own RPC provider to avoid shared rate limits
* **Network Flexibility**: Pass custom Monaco network configurations for different environments
**Breaking Changes**
* The SDK now requires `seiRpcUrl` to be provided during initialization
* This change affects all SDK instantiations in both TypeScript and React packages
### Why This Change?
Public RPC endpoints are heavily rate-limited and can cause reliability issues in production applications. By requiring each application to configure their own RPC provider, Monaco SDK enables:
* **Better Performance**: Dedicated RPC endpoints provide consistent performance
* **No Rate Limiting**: Avoid throttling from shared public endpoints
* **Production Ready**: Use premium RPC providers for mission-critical applications
* **Custom Infrastructure**: Full control over your blockchain connectivity
### Technical Details
**TypeScript SDK - New Initialization**
```typescript theme={null}
import { MonacoSDK } from '@0xmonaco/core';
import { createWalletClient, custom } from 'viem';
import { seiTestnet } from 'viem/chains';
const walletClient = createWalletClient({
chain: seiTestnet,
transport: custom(window.ethereum)
});
// New: seiRpcUrl is now required
const sdk = new MonacoSDK({
walletClient,
network: 'testnet',
seiRpcUrl: 'https://evm-rpc.sei-apis.com' // Your RPC endpoint
});
```
**React Package - MonacoProvider Updates**
```typescript theme={null}
import { MonacoProvider } from '@0xmonaco/react';
function App() {
return (
);
}
```
**Custom Network Configuration**
For advanced use cases, you can now pass custom network configurations:
```typescript theme={null}
import { MonacoSDK, MonacoNetwork } from '@0xmonaco/core';
const customNetwork: MonacoNetwork = {
apiBaseUrl: 'https://your-custom-api.com',
wsBaseUrl: 'wss://your-custom-ws.com',
chainId: 1328, // Sei testnet
vaultAddress: '0x...',
};
const sdk = new MonacoSDK({
walletClient,
network: customNetwork, // Pass custom network object
seiRpcUrl: 'https://your-rpc-provider.com'
});
```
### RPC Provider Options
You can use various RPC providers for Sei:
**Sei Public RPCs** (Rate Limited)
* Testnet: `https://evm-rpc.sei-apis.com`
* Mainnet: `https://evm-rpc.sei-apis.com` (update to mainnet endpoint)
**Recommended Providers** (For Production)
* See [Sei's official RPC providers list](https://docs.sei.io/learn/rpc-providers) for recommended providers
* Self-hosted Sei nodes for maximum control
### Migration from v0.3.x
**Before (v0.3.x)**
```typescript theme={null}
const sdk = new MonacoSDK({
walletClient,
network: 'testnet'
});
```
**After (v0.4.0)**
```typescript theme={null}
const sdk = new MonacoSDK({
walletClient,
network: 'testnet',
seiRpcUrl: 'https://evm-rpc.sei-apis.com' // Required
});
```
**React Migration**
**Before (v0.3.x)**
```typescript theme={null}
```
**After (v0.4.0)**
```typescript theme={null}
```
### Environment Variables Best Practice
Store your RPC URL in environment variables:
```typescript theme={null}
// .env
VITE_SEI_RPC_URL=https://evm-rpc.sei-apis.com
// Your code
const sdk = new MonacoSDK({
walletClient,
network: 'testnet',
seiRpcUrl: import.meta.env.VITE_SEI_RPC_URL
});
```
### Packages Updated
* `@0xmonaco/core@0.4.0`
* `@0xmonaco/types@0.4.0`
* `@0xmonaco/contracts@0.4.0`
* `@0xmonaco/react@0.4.0`
### Installation
```bash theme={null}
npm install @0xmonaco/core@0.4.0
npm install @0xmonaco/react@0.4.0 # For React applications
```
### Breaking Changes Summary
| Change | Impact | Migration |
| - | - | - |
| `seiRpcUrl` now required | High - Affects all SDK initializations | Add `seiRpcUrl` parameter to SDK constructor |
| `network` parameter enhanced | Low - Backward compatible | Optionally use custom network objects |
### Documentation Updates
* [Build a Trading App](/examples/build-trading-app) - Integration guide with RPC configuration
* [Quickstart](/quickstart) - New SDK initialization examples
* [TypeScript SDK](/sdk/typescript/intro) - Complete setup guide
* [React SDK](/sdk/react/intro) - MonacoProvider configuration
### Related Changes
This SDK release corresponds to infrastructure improvements:
* Better separation between Monaco API and blockchain RPC
* Improved reliability through dedicated RPC endpoints
* Foundation for multi-network support
* Enhanced developer control over infrastructure
### Support
If you need help choosing an RPC provider or configuring your SDK, join our Discord community or contact the Monaco Protocol team.
***
*Note: While public Sei RPCs work for development and testing, we strongly recommend using a dedicated RPC provider for production applications to ensure reliability and performance.*
# V0.4.1
Source: https://docs.0xmonaco.com/releases/v0.4.1
# Monaco Protocol SDK v0.4.1
## Indexer Improvements & Bug Fixes
This patch release focuses on backend infrastructure improvements, primarily migrating the indexer from the deprecated ethers.rs library to alloy, along with critical bug fixes for withdrawal event handling.
### What's New
**SDK Improvements**
* **TypeScript 5.9+**: Full type safety and IntelliSense support with latest TypeScript
* **viem 2.31+**: Updated to latest viem for improved blockchain interactions
* **Better Type Inference**: Improved TypeScript configuration for optimal developer experience
**Indexer Modernization**
* **Alloy Migration**: Replaced deprecated ethers.rs with modern alloy library for blockchain interactions
* **WebSocket Event Listening**: Native WebSocket support for vault event monitoring (no longer requires special RPC provider filters)
* **Broader RPC Compatibility**: Works with any standard EVM provider, including free tiers like Rhino
* **Repository Pattern**: Cleaner data access layer with dedicated repositories for applications, blockchain events, and processed blocks
* **Enhanced Gap Detection**: Improved logic for detecting and backfilling missed blockchain events
**Bug Fixes**
* **Withdrawal Events**: Fixed withdrawal event signature causing withdrawal events not to be indexed correctly
* **Whitelist App**: Resolved critical bug in the whitelist application
* **Configuration**: Improved environment variable naming consistency across indexer services
### Technical Details
**SDK Dependencies**
This release updates core SDK dependencies for better performance and type safety:
* **TypeScript 5.9+**: Full type safety and IntelliSense support
* **viem 2.31+**: Modern Ethereum library for blockchain interactions
### TypeScript Configuration
For optimal type inference and IntelliSense support, use the following TypeScript configuration:
```json theme={null}
{
"compilerOptions": {
"target": "ES2020",
"module": "nodenext",
"moduleResolution": "nodenext",
"strict": true,
"esModuleInterop": true,
"resolveJsonModule": true
}
}
```
**New Indexer Architecture**
The indexer has been refactored to follow best practices with repository pattern implementation:
```rust theme={null}
// New repositories for cleaner data access
ApplicationRepository
BlockchainEventsRepository
ProcessedBlocksRepository
TradingPairsRepository
```
**WebSocket-Based Event Monitoring**
Previous versions required RPC providers with special filtering capabilities. v0.4.1 uses native WebSocket connections:
```
Before: HTTP polling with filters (limited provider support)
After: WebSocket subscriptions (works with any EVM provider)
```
**Configuration Updates**
Environment variable naming has been standardized. Indexer operators should update their configuration:
```env theme={null}
# Updated variable names for consistency
INDEXER_RPC_URL=https://your-rpc-provider.com
INDEXER_POLLING_INTERVAL_MS=12000
DATABASE_URL=postgresql://...
```
**Improved Backfilling**
Enhanced gap detection and automatic backfilling ensures no events are missed:
* Detects gaps in processed blocks
* Automatically backfills missing events
* Better logging for monitoring indexer health
* Improved error handling and retry logic
### Migration Guide
**For Indexer Operators**
If you're running your own indexer, update your environment configuration:
1. Review the updated `.env.example` file in the `indexer/` directory
2. Update environment variable names to match new conventions
3. Restart the indexer service
4. Monitor logs for successful WebSocket connection
```bash theme={null}
# Restart indexer after configuration updates
cargo run -p indexer
```
**For SDK Users**
Update your dependencies to benefit from the latest improvements:
```bash theme={null}
npm install @0xmonaco/core@0.4.1 viem@^2.31.0
```
**Recommended TypeScript Configuration:**
If you haven't already, update your `tsconfig.json` for better type inference:
```json theme={null}
{
"compilerOptions": {
"target": "ES2020",
"module": "nodenext",
"moduleResolution": "nodenext",
"strict": true,
"esModuleInterop": true,
"resolveJsonModule": true
}
}
```
**What's Improved:**
* Better type safety with TypeScript 5.9+
* Enhanced blockchain interactions with viem 2.31+
* Improved IntelliSense and autocomplete
* No breaking changes to existing code
### Packages Updated
This release primarily affects backend services:
* **Indexer Service**: v0.4.1 with alloy migration
* **Data Layer**: Enhanced with new repositories
* **Infrastructure**: Updated Docker and configuration files
SDK packages remain at v0.4.1:
* `@0xmonaco/core@0.4.1`
* `@0xmonaco/types@0.4.1`
* `@0xmonaco/contracts@0.4.1`
* `@0xmonaco/react@0.4.1`
### Installation
```bash theme={null}
npm install @0xmonaco/core@0.4.1
npm install @0xmonaco/react@0.4.1 # For React applications
```
Or install the latest version without specifying:
```bash theme={null}
npm install @0xmonaco/core
npm install @0xmonaco/react # For React applications
```
### Benefits
**For SDK Users**
* Enhanced type safety with TypeScript 5.9+ (better IntelliSense and autocomplete)
* Improved blockchain interactions with viem 2.31+
* Better developer experience with optimized TypeScript configuration
**For Backend Developers**
* More reliable event indexing
* Broader RPC provider compatibility
* Better error messages and logging
* Cleaner codebase following repository pattern
**For Infrastructure Operators**
* Reduced RPC provider requirements (no special filters needed)
* More cost-effective infrastructure (free tier providers work)
* Improved monitoring with better gap detection
* Enhanced reliability with automatic backfilling
### Related Changes
This release includes infrastructure improvements merged from multiple PRs:
* Indexer refactored to use alloy with repository pattern
* Enhanced vault event handling with WebSocket support
* Docker and environment configuration updates
* Comprehensive test coverage additions
### Testing & Coverage
Enhanced test suite with new integration and unit tests:
* Vault listener integration tests
* Repository unit tests
* Event handling edge cases
* Gap detection and backfilling tests
***
## Commit References
* `3a0d893d` - Refactor indexer to use alloy
* `d352c7d5` - Fix Withdrawal Event Signature
* `7ac2afd4` - Fixed critical bug in whitelist app
* `764d87ed` - Move alloy to workspace, cleanup dependencies
***
*Note: This is a patch release focused on backend infrastructure. No breaking changes for SDK users. Indexer operators should review and update their environment configuration.*
# V0.5.0
Source: https://docs.0xmonaco.com/releases/v0.5.0
# Monaco Protocol SDK v0.5.0
## WebSocket API Refresh & Trades Endpoint
This release introduces a new Trades REST API, real-time trades WebSocket channel, and updates the WebSocket SDK to use a cleaner functional pattern.
### What's New
**Trades API**
* **REST Endpoint**: `GET /api/v1/trades/{trading_pair_id}` for fetching recent trade history
* **WebSocket Channel**: `trades:{trading_pair_id}` for real-time trade event streaming
* **Paginated Results**: Support for `skip` and `limit` query parameters
**WebSocket SDK Improvements**
* **Functional Pattern**: New cleaner API with `sdk.ws.orders()`, `sdk.ws.orderbook()`, `sdk.ws.ohlcv()`, `sdk.ws.trades()`
* **Unsubscribe Functions**: All subscription methods now return cleanup functions
* **Required wsUrl**: WebSocket URL is now a required configuration parameter
**Documentation Updates**
* Complete WebSocket documentation rewrite with updated examples
* New Trades API reference documentation
* Fixed balance endpoint path parameter (`asset_id` instead of `token`)
### Breaking Changes
**WebSocket Channels Now Use UUIDs**
All WebSocket channels (orders, orderbook, ohlcv, trades) now require trading pair IDs (UUIDs) instead of symbols:
```typescript theme={null}
// Before (v0.4.x)
sdk.websocket.ohlcv.subscribeToOHLCV('BTC/USDC', 'SPOT', '1m', handler);
// After (v0.5.0)
const pair = await sdk.market.getTradingPairBySymbol('BTC-USDC');
sdk.ws.ohlcv(pair.id, 'SPOT', '1m', handler); // Use UUID
```
**WebSocket API Pattern Change**
The WebSocket SDK has been refactored from a class-based pattern to a functional pattern:
```typescript theme={null}
// Before (v0.4.x)
await sdk.websocket.orders.connect();
sdk.websocket.orders.subscribeToOrderEvents('BTC/USDC', 'SPOT', handler);
// After (v0.5.0)
await sdk.ws.connect();
const unsubscribe = sdk.ws.orders('trading-pair-id', 'SPOT', handler);
// Later: unsubscribe();
```
**Required wsUrl Parameter**
The `wsUrl` parameter is now required when initializing the SDK:
```typescript theme={null}
const sdk = new MonacoSDK({
walletClient,
network: 'testnet',
seiRpcUrl: 'https://evm-rpc.sei-apis.com',
wsUrl: 'wss://staging.apimonaco.xyz/ws' // Now required
});
```
### New Features
**Trades REST API**
Fetch recent trades for any trading pair:
```typescript theme={null}
const trades = await sdk.trades.getTrades(tradingPairId, {
skip: 0,
limit: 25
});
// Response
{
trades: [{
eventType: 'trade',
pairId: '...',
tradingMode: 'SPOT',
data: {
tradeId: '...',
price: '50000.00',
quantity: '0.5',
makerSide: 'BUY',
executedAt: '2024-01-15T10:30:00Z'
}
}],
skip: 0,
limit: 25
}
```
**Trades WebSocket Channel**
Subscribe to real-time trade events:
```typescript theme={null}
await sdk.ws.connect();
const unsubscribe = sdk.ws.trades(tradingPairId, (event) => {
console.log('Trade:', event.data.price, event.data.quantity);
});
// Later, to unsubscribe:
unsubscribe();
```
**Updated WebSocket Subscriptions**
All WebSocket channels now follow the same functional pattern:
```typescript theme={null}
// Orders (requires authentication)
const unsubOrders = sdk.ws.orders(tradingPairId, 'SPOT', (event) => {
console.log('Order event:', event.eventType);
});
// Orderbook (public)
const unsubOrderbook = sdk.ws.orderbook(tradingPairId, 'SPOT', 1, 'BASE', (event) => {
console.log('Best bid:', event.bestBid);
});
// OHLCV (public)
const unsubOHLCV = sdk.ws.ohlcv(tradingPairId, 'SPOT', '1m', (event) => {
console.log('Candle:', event.candlestick);
});
// Trades (public)
const unsubTrades = sdk.ws.trades(tradingPairId, (event) => {
console.log('Trade:', event.data);
});
```
### Migration Guide
**1. Update SDK Initialization**
Add the required `wsUrl` parameter:
```typescript theme={null}
const sdk = new MonacoSDK({
walletClient,
network: 'testnet',
seiRpcUrl: 'https://evm-rpc.sei-apis.com',
wsUrl: 'wss://staging.apimonaco.xyz/ws' // Add this
});
```
**2. Update WebSocket Code**
Replace the old pattern with the new functional approach:
```typescript theme={null}
// Old
await sdk.websocket.orders.connect();
sdk.websocket.orders.subscribeToOrderEvents('BTC/USDC', 'SPOT', handler);
// New
await sdk.ws.connect();
const unsubscribe = sdk.ws.orders(tradingPairId, 'SPOT', handler);
```
**3. Use Trading Pair IDs**
WebSocket subscriptions now use trading pair IDs (UUIDs) instead of symbols:
```typescript theme={null}
// Get trading pair ID first
const pair = await sdk.market.getTradingPairBySymbol('BTC-USDC');
// Use the ID for subscriptions
sdk.ws.orders(pair.id, 'SPOT', handler);
```
### Packages Updated
* `@0xmonaco/core@0.5.0`
* `@0xmonaco/types@0.5.0`
* `@0xmonaco/react@0.5.0`
### Installation
```bash theme={null}
npm install @0xmonaco/core@0.5.0
npm install @0xmonaco/react@0.5.0 # For React applications
```
***
*This release includes breaking changes to the WebSocket API. Please review the migration guide before upgrading.*
# V0.5.1
Source: https://docs.0xmonaco.com/releases/v0.5.1
# Monaco Protocol v0.5.1
> **Note:** These are historical release notes for v0.5.1 and describe the architecture as it shipped at that time. Redis (and its ElastiCache deployment) has since been removed. The current architecture is PostgreSQL (durable source of truth) + NATS JetStream (durable log and pub/sub fan-out) + an in-memory matching engine (DashMap). References to Redis below reflect the v0.5.1 design only.
## Architecture Evolution
This release marks a significant evolution in Monaco's infrastructure, transitioning from a monolithic API to a **distributed microservices architecture** designed for high-frequency trading workloads.
### High-Level Architecture
```
+------------------+
| Application |
| Load Balancer |
+--------+---------+
|
+------------------------+------------------------+
| | |
v v v
+---------------+ +---------------+ +---------------+
| HTTP API | | WebSocket | | gRPC API |
| (REST) | | API | | (Proto/HTTP2) |
| Port 8080 | | Port 8080 | | Port 50051 |
+-------+-------+ +-------+-------+ +-------+-------+
| | |
+------------------------+------------------------+
|
v
+------------------------+
| Matching Engine |
| (Order Processing) |
+------------------------+
|
+------------------------+------------------------+
| |
v v
+---------------+ +---------------+
| Redis | | PostgreSQL |
| (Hot Path) | | (Persistence) |
+---------------+ +---------------+
```
### Microservices Breakdown
| Service | Purpose | Protocol | Port |
| - | - | - | - |
| **api-gateway-rust** | REST API for trading, accounts, market data | HTTP/1.1, HTTPS | 8080 |
| **ws-api** | Real-time orderbook & trade streaming | WebSocket | 8080 |
| **grpc-api** | High-performance trading API | gRPC/HTTP2 | 50051 |
| **matching-engine** | Order matching & trade execution | Internal | - |
### Path-Based Routing
All services are accessible through a single domain with intelligent path-based routing:
```
https://{host}/ → REST API (api-gateway)
wss://{host}/ws → WebSocket API (ws-api)
https://{host} → gRPC API over HTTP/2 method paths (grpc-api)
```
***
## Performance Architecture
### The Monaco Speed Layer
Monaco achieves sub-millisecond order processing through a carefully designed **dual-storage architecture**:
```
Order Flow
|
v
+-------------------+
| Balance Check & |
| Lock (Atomic) | <-- Redis (< 1ms)
+-------------------+
|
v
+-------------------+
| Order Matching |
| (In-Memory) | <-- Matching Engine
+-------------------+
|
v
+-------------------+
| Trade Settlement |
| (Atomic) | <-- Redis (< 1ms)
+-------------------+
|
v
+-------------------+
| Persistence |
| (Fire & Forget) | <-- PostgreSQL (async)
+-------------------+
```
**Key Innovation**: Our proprietary Redis layer handles all balance operations with **custom atomic scripts** that ensure consistency while maintaining sub-millisecond latency. This allows us to process the entire order lifecycle—from balance validation to trade settlement—without waiting for database round-trips.
### Latency Targets
| Operation | Target | Description |
| - | - | - |
| Balance Lock | \< 1ms | Atomic check-and-lock |
| Order Matching | \< 1ms | In-memory orderbook |
| Trade Settlement | \< 2ms | Multi-party atomic update |
| Total E2E | \< 5ms | Full order lifecycle |
***
## Real-Time Data Streaming
### WebSocket Architecture
The dedicated WebSocket service provides real-time market data with minimal latency:
```
+-------------+ +----------+ +---------------+ +--------+
| Client | <-> | ALB | <-> | ws-api | <-> | Redis |
| (Browser/ | | (Sticky | | (Pub/Sub | | Pub/Sub|
| Bot) | | Session)| | Consumer) | | |
+-------------+ +----------+ +---------------+ +--------+
^
|
+---------------+
| Matching |
| Engine |
| (Publisher) |
+---------------+
```
**Available Channels:**
* `orderbook:{trading_pair_id}` - Real-time orderbook updates
* `trades:{trading_pair_id}` - Trade executions
* `user:{user_id}` - Private order updates
***
## gRPC API (New)
### Why gRPC?
| Feature | REST | gRPC |
| - | - | - |
| Serialization | JSON (text) | Protobuf (binary) |
| Connection | HTTP/1.1 (new per request) | HTTP/2 (multiplexed) |
| Typing | Runtime validation | Compile-time types |
| Streaming | Polling/WebSocket | Native bidirectional |
### Quick Start
```typescript theme={null}
import * as grpc from '@grpc/grpc-js';
const client = new proto.public_api.v1.OrdersService(
'staging.apimonaco.xyz:443',
grpc.credentials.createSsl()
);
// Create order with sub-5ms latency
const order = await client.CreateOrder({
trading_pair_id: 'uuid',
order_type: 'LIMIT',
side: 'BUY',
price: '50000.00',
quantity: '0.1',
time_in_force: 'GTC'
}, metadata);
```
### Service Reference
| Service | Methods | Auth |
| - | - | - |
| `AuthService` | Challenge, Verify, Refresh, Revoke | No |
| `OrdersService` | Create, Cancel, Replace, List | JWT |
| `AccountsService` | Profile, Balances, Movements | JWT |
| `MarketDataService` | Pairs, Orderbook, Trades, Candles | No |
| `TransfersService` | Deposit/Withdraw signatures | JWT |
***
## Deployment Architecture
### Container Orchestration
Each microservice runs as an independent ECS service with auto-scaling:
```
+------------------------------------------+
| AWS ECS Cluster |
| |
| +-------------+ +-------------+ |
| | api-gateway | | api-gateway | x N |
| | Task 1 | | Task 2 | |
| +-------------+ +-------------+ |
| |
| +-------------+ +-------------+ |
| | ws-api | | ws-api | x N |
| | Task 1 | | Task 2 | |
| +-------------+ +-------------+ |
| |
| +-------------+ +-------------+ |
| | grpc-api | | grpc-api | x N |
| | Task 1 | | Task 2 | |
| +-------------+ +-------------+ |
| |
| +-------------+ |
| | matching | (Single instance |
| | engine | per trading pair) |
| +-------------+ |
+------------------------------------------+
```
### Horizontal Scaling
* **Stateless APIs**: HTTP, WebSocket, and gRPC services scale horizontally
* **Matching Engine**: Partitioned by quote token for parallel processing
* **Redis**: ElastiCache cluster with read replicas
* **PostgreSQL**: RDS with read replicas for query scaling
***
## Migration Guide
### From v0.5.0
No breaking changes. The REST API remains fully compatible. New services are additive:
1. **WebSocket**: Now on dedicated service (improved reliability)
2. **gRPC**: New option for latency-sensitive applications
### Choosing Your API
| Use Case | Recommended API |
| - | - |
| Web applications | REST + WebSocket |
| Mobile apps | REST + WebSocket |
| Trading bots | gRPC |
| Market makers | gRPC + WebSocket |
| Backend services | gRPC |
***
## What's Next
* **Streaming gRPC**: Server-side streaming for real-time data
* **FIX Protocol**: Industry-standard trading protocol support
* **Colocation**: Reduced latency for institutional traders
***
*For detailed API documentation, see the [REST API](/api-reference) or [WebSocket API](/websocket) sections.*
# V0.5.2
Source: https://docs.0xmonaco.com/releases/v0.5.2
# Monaco Protocol v0.5.2
## gRPC API Introduction
This release introduces the Monaco gRPC API, a high-performance alternative to our REST API designed for low-latency trading applications. The gRPC API provides the same functionality as the REST API with significantly reduced overhead and native streaming support.
### What's New
**gRPC API**
* **Full Trading Support**: Create, cancel, and replace orders via gRPC
* **Real-time Market Data**: Orderbook snapshots, recent trades, and OHLCV candles
* **Account Management**: Profile, balances, movements, and sub-account limits
* **Authentication**: Wallet-based (SIWE) and backend service authentication
* **Transfers**: Deposit and withdrawal signature generation
### gRPC Services Overview
The Monaco gRPC API exposes the following services:
| Service | Description | Auth Required |
| - | - | - |
| `AuthService` | Authentication (challenge, verify, refresh, revoke) | No |
| `OrdersService` | Order management (create, cancel, replace, list) | Yes (JWT) |
| `AccountsService` | Account info, balances, movements, sub-accounts | Yes (JWT) |
| `MarketDataService` | Trading pairs, orderbook, trades, candles | No |
| `FeesService` | Fee simulation for orders | Yes (JWT) |
| `ApplicationsService` | Backend-only app management | Yes (x-server-key) |
| `TransfersService` | Deposit/withdrawal signatures | Yes (JWT) |
| `FaucetService` | Testnet token minting | Yes (JWT) |
| `HealthService` | Service health checks | No |
### Connection Details
The gRPC API is exposed via path-based routing on the same ALB as the REST API:
```
Protocol: HTTP/2 with TLS (gRPC over HTTPS)
Package: public_api.v1
Environments:
Staging: staging.apimonaco.xyz
Routing on ALB:
- REST API: https://staging.apimonaco.xyz/ → api-gateway (port 8080)
- WebSocket: wss://staging.apimonaco.xyz/ws → ws-api (port 8080)
- gRPC: https://staging.apimonaco.xyz → grpc-api (port 50051)
```
The ALB routes requests based on path prefixes:
* `/ws/*` routes to the WebSocket service
* gRPC method paths such as `/monaco.api.*` route to the gRPC service with `protocolVersion: GRPC`
* All other paths route to the REST API
### Quick Start
**1. Generate client code from proto**
Download the proto file and generate client code for your language:
```bash theme={null}
# For Node.js/TypeScript (using gRPC-Web for ALB path-based routing)
npm install @grpc/grpc-js @grpc/proto-loader
# For Rust
# Add to Cargo.toml: tonic, prost
```
**2. Connecting to the gRPC API**
Since the gRPC API uses HTTP/2 method-path routing on the ALB, clients need to connect with the correct configuration:
```typescript theme={null}
import * as grpc from '@grpc/grpc-js';
import * as protoLoader from '@grpc/proto-loader';
// Load proto definition
const packageDefinition = protoLoader.loadSync('public_api.proto');
const proto = grpc.loadPackageDefinition(packageDefinition);
// Environment configuration
const GRPC_HOST = 'staging.apimonaco.xyz';
// Create client - the ALB routes gRPC method paths to the gRPC service
const client = new proto.public_api.v1.AuthService(
`${GRPC_HOST}:443`,
grpc.credentials.createSsl()
);
```
**3. Authentication Flow**
```typescript theme={null}
// Step 1: Get challenge
const challenge = await new Promise((resolve, reject) => {
client.Challenge({
address: '0xYourWalletAddress',
client_id: 'your-client-id' // optional
}, (err, response) => {
if (err) reject(err);
else resolve(response);
});
});
// Step 2: Sign message with wallet (using viem, ethers, etc.)
const signature = await wallet.signMessage(challenge.message);
// Step 3: Verify and get tokens
const tokens = await new Promise((resolve, reject) => {
client.Verify({
address: '0xYourWalletAddress',
signature: signature,
nonce: challenge.nonce
}, (err, response) => {
if (err) reject(err);
else resolve(response);
});
});
console.log('Access Token:', tokens.access_token);
console.log('Refresh Token:', tokens.refresh_token);
```
**4. Creating an Order**
```typescript theme={null}
// Create authenticated client with JWT
const metadata = new grpc.Metadata();
metadata.set('authorization', `Bearer ${tokens.access_token}`);
const ordersClient = new proto.public_api.v1.OrdersService(
`${GRPC_HOST}:443`,
grpc.credentials.createSsl()
);
const orderResponse = await new Promise((resolve, reject) => {
ordersClient.CreateOrder({
trading_pair_id: 'uuid-of-trading-pair',
order_type: 'LIMIT',
side: 'BUY',
price: '50000.00',
quantity: '0.1',
time_in_force: 'GTC'
}, metadata, (err, response) => {
if (err) reject(err);
else resolve(response);
});
});
console.log('Order ID:', orderResponse.order_id);
console.log('Status:', orderResponse.status);
```
**5. Getting Market Data (No Auth Required)**
```typescript theme={null}
const marketClient = new proto.public_api.v1.MarketDataService(
`${GRPC_HOST}:443`,
grpc.credentials.createSsl()
);
// List trading pairs
const pairs = await new Promise((resolve, reject) => {
marketClient.ListTradingPairs({
is_active: true,
limit: 10
}, (err, response) => {
if (err) reject(err);
else resolve(response);
});
});
// Get orderbook
const orderbook = await new Promise((resolve, reject) => {
marketClient.GetOrderbook({
trading_pair_id: 'uuid-of-trading-pair',
levels: 20
}, (err, response) => {
if (err) reject(err);
else resolve(response);
});
});
// Get candles
const candles = await new Promise((resolve, reject) => {
marketClient.GetCandles({
trading_pair_id: 'uuid-of-trading-pair',
interval: '1h',
limit: 100
}, (err, response) => {
if (err) reject(err);
else resolve(response);
});
});
```
**Note on Path-Based Routing**
The ALB listener rule routes gRPC method paths such as `/monaco.api.*` and `/grpc.health.v1.*` to the gRPC service. Clients connect to the host on `443` and do not include a `/grpc` prefix.
### Service Reference
#### AuthService
| Method | Request | Response | Description |
| - | - | - | - |
| `Challenge` | `ChallengeRequest` | `ChallengeResponse` | Generate authentication challenge |
| `Verify` | `VerifyRequest` | `VerifyResponse` | Verify signature and get tokens |
| `Refresh` | `RefreshRequest` | `RefreshResponse` | Refresh access token |
| `Revoke` | `RevokeRequest` | `RevokeResponse` | Revoke current session |
#### OrdersService
| Method | Request | Response | Description |
| - | - | - | - |
| `CreateOrder` | `PublicCreateOrderRequest` | `PublicCreateOrderResponse` | Create a new order |
| `CancelOrder` | `PublicCancelOrderRequest` | `PublicCancelOrderResponse` | Cancel an existing order |
| `ReplaceOrder` | `PublicReplaceOrderRequest` | `PublicReplaceOrderResponse` | Replace an order (atomic cancel + create) |
| `GetOrder` | `GetOrderRequest` | `OrderResponse` | Get order by ID |
| `ListOrders` | `ListOrdersRequest` | `ListOrdersResponse` | List orders with pagination |
#### AccountsService
| Method | Request | Response | Description |
| - | - | - | - |
| `GetProfile` | `GetProfileRequest` | `UserProfile` | Get user profile with balances |
| `GetBalances` | `GetBalancesRequest` | `BalancesResponse` | Get paginated balances |
| `GetBalanceByAsset` | `GetBalanceByAssetRequest` | `AccountBalance` | Get balance for specific token |
| `GetMovements` | `GetMovementsRequest` | `MovementsResponse` | Get ledger movements |
| `ListSubAccounts` | `ListSubAccountsRequest` | `ListSubAccountsResponse` | List sub-accounts (master only) |
| `CreateLimit` | `CreateLimitRequest` | `SubAccountLimit` | Create sub-account limit |
| `UpdateLimit` | `UpdateLimitRequest` | `SubAccountLimit` | Update sub-account limit |
| `GetLimits` | `GetLimitsRequest` | `LimitsResponse` | Get sub-account limits |
| `DeleteLimit` | `DeleteLimitRequest` | `DeleteLimitResponse` | Delete sub-account limit |
#### MarketDataService (Public)
| Method | Request | Response | Description |
| - | - | - | - |
| `ListTradingPairs` | `ListTradingPairsRequest` | `ListTradingPairsResponse` | List trading pairs |
| `GetTradingPair` | `GetTradingPairRequest` | `TradingPairResponse` | Get trading pair by ID |
| `GetOrderbook` | `GetOrderbookRequest` | `OrderbookResponse` | Get orderbook snapshot |
| `GetTrades` | `GetTradesRequest` | `TradesResponse` | Get recent trades |
| `GetCandles` | `GetCandlesRequest` | `CandlesResponse` | Get OHLCV candles |
#### FeesService
| Method | Request | Response | Description |
| - | - | - | - |
| `SimulateFees` | `SimulateFeesRequest` | `SimulateFeesResponse` | Simulate fees for an order |
#### TransfersService
| Method | Request | Response | Description |
| - | - | - | - |
| `GetDepositSignature` | `DepositSignatureRequest` | `DepositSignatureResponse` | Generate deposit signature |
| `GetWithdrawSignature` | `WithdrawSignatureRequest` | `WithdrawSignatureResponse` | Generate withdrawal signature |
#### ApplicationsService (Backend Only)
| Method | Request | Response | Description |
| - | - | - | - |
| `GetConfig` | `GetConfigRequest` | `ApplicationConfig` | Get application configuration |
| `ListAppOrders` | `ListAppOrdersRequest` | `ListAppOrdersResponse` | List orders for app |
| `ListAppUsers` | `ListAppUsersRequest` | `ListAppUsersResponse` | List users for app |
| `ListAppMovements` | `ListAppMovementsRequest` | `ListAppMovementsResponse` | List movements for app |
| `ListAppBalances` | `ListAppBalancesRequest` | `ListAppBalancesResponse` | List balances for app |
#### FaucetService (Testnet Only)
| Method | Request | Response | Description |
| - | - | - | - |
| `MintTokens` | `MintTokensRequest` | `MintTokensResponse` | Mint all testnet tokens to user |
### Order Types and Time-in-Force
**Order Types:**
* `LIMIT` - Limit order (price required)
* `MARKET` - Market order (no price, uses slippage tolerance)
**Time-in-Force Options:**
* `GTC` - Good Till Cancelled (default)
* `IOC` - Immediate or Cancel
* `FOK` - Fill or Kill
* `GTD` - Good Till Date (requires `expiration_date`)
### Error Handling
gRPC uses standard status codes. Common codes:
| Code | Description |
| - | - |
| `OK` | Success |
| `INVALID_ARGUMENT` | Invalid request parameters |
| `NOT_FOUND` | Resource not found |
| `UNAUTHENTICATED` | Missing or invalid authentication |
| `PERMISSION_DENIED` | Insufficient permissions |
| `FAILED_PRECONDITION` | Operation cannot be performed in current state |
| `UNAVAILABLE` | Service temporarily unavailable |
### Proto File
The complete proto definition is available at:
* **GitHub**: `protos/*`
### Technical Details
**Server Implementation**
* Built with Tonic (Rust gRPC framework)
* HTTP/2 multiplexing for concurrent requests
* Standard gRPC health checks (`grpc.health.v1.Health`)
* OpenTelemetry integration for distributed tracing
**Performance Benefits over REST**
* Binary Protocol Buffers serialization (smaller payloads)
* HTTP/2 connection multiplexing (no head-of-line blocking)
* Native streaming support (future: real-time orderbook updates)
* Strong typing with code generation
### Current Limitations
Please note:
* **Stability**: API may change before stable release
* **Streaming**: Server-side streaming not yet implemented
* **Rate Limits**: Same limits as REST API apply
* **Documentation**: Proto file is the source of truth
### Migration from REST
The gRPC API provides equivalent functionality to all REST endpoints. Key differences:
| REST | gRPC |
| - | - |
| `POST /auth/challenge` | `AuthService.Challenge` |
| `POST /auth/verify` | `AuthService.Verify` |
| `POST /orders` | `OrdersService.CreateOrder` |
| `DELETE /orders/:id` | `OrdersService.CancelOrder` |
| `GET /market/trading-pairs` | `MarketDataService.ListTradingPairs` |
| `GET /orderbook/:id` | `MarketDataService.GetOrderbook` |
### Feedback
We welcome feedback on:
* API design and ergonomics
* Missing functionality
* Performance issues
* Documentation clarity
Please share your feedback with the Monaco team.
***
## Testnet Faucet & New Assets
### New Testnet Assets
This release includes a migration that resets testnet assets with the following tokens:
| Token | Contract Address | Decimals |
| - | - | - |
| WBTC | `0xF117edeAf9F3eBBd4F32029297FB6b9fCCF190E3` | 8 |
| WETH | `0x56B49bF1d089B1d8f95B9Be6Ae3aA071593c0C00` | 18 |
| WSOL | `0x410CC773b96a5dA34776Efa93A278acac0A70A7B` | 9 |
| WSEI | `0x298b267Dd31eE390DC5a9291216423f4cFa03F66` | 18 |
| USDC | `0x23E459c24C4925BA89205c400dcECa7aBC11723d` | 6 |
| SPX | `0xFdFd38E526cDdf6Ab76E87d656fd896b6f45D2AC` | 8 |
| AMZN | `0x8977fe7E64cb358174f906D9691915299355C7b1` | 8 |
| GOOG | `0x56e350af4b6C14D7Af40e496230D64c43122814E` | 8 |
| META | `0x9D1a2eaaD89F77B97749326463F921A28D010a18` | 8 |
| NVDA | `0x8eED6535E1A806aadbBc478F915d64D70d531D39` | 8 |
| TSLA | `0xFd27738B096AB3c2947dC31d9B3af2c572C35c91` | 8 |
### Trading Pairs
All tokens are paired against USDC:
* WBTC/USDC, WETH/USDC, WSOL/USDC, WSEI/USDC
* SPX/USDC, AMZN/USDC, GOOG/USDC, META/USDC, NVDA/USDC, TSLA/USDC
### Faucet Endpoint
A new faucet endpoint allows authenticated users to mint all testnet tokens at once.
**Endpoint:** `POST /api/v1/faucet/mint`
**Authentication:** Required (JWT Bearer token)
**Rate Limit:** Configurable via `faucet_daily_limit` feature flag (default: 1 request per 24 hours)
**Response:**
```json theme={null}
{
"success": true,
"minted": [
{
"asset_id": "uuid",
"symbol": "WBTC",
"amount": "5",
"tx_hash": "0x..."
},
{
"asset_id": "uuid",
"symbol": "WETH",
"amount": "100",
"tx_hash": "0x..."
}
],
"failed": [],
"remaining_requests_24h": 0
}
```
**Faucet Amounts per Token:**
| Token | Amount |
| - | - |
| WBTC | 5 |
| WETH | 100 |
| WSOL | 100 |
| WSEI | 1,000 |
| USDC | 10,000 |
| SPX, AMZN, GOOG, META, NVDA, TSLA | 50 each |
***
*Note: The REST API remains fully supported.*
# V0.5.3
Source: https://docs.0xmonaco.com/releases/v0.5.3
# Monaco Protocol SDK v0.5.3
## Candlestick API Refactor & Market Metadata
This release refactors the candlestick API to use trading pair UUIDs and introduces TradingView-compliant query patterns, along with new market metadata capabilities.
### Breaking Changes
**Candlestick API Signature Change:**
The `getCandlesticks` method now requires a trading pair UUID instead of a symbol string, and uses an options object for parameters.
```typescript theme={null}
// Before (v0.5.2)
sdk.market.getCandlesticks("BTC-USDC", "1h", startTime, endTime);
// After (v0.5.3)
const pair = await sdk.market.getTradingPairBySymbol("BTC-USDC");
sdk.market.getCandlesticks(pair.id, "1h", { startTime, endTime, limit: 350 });
```
### New Features
**Flexible Candlestick Queries:**
* Optional `startTime`, `endTime`, and `limit` parameters via `GetCandlesticksParams`
* Support for backward pagination (`endTime` + `limit`) — TradingView's countBack pattern
* Support for forward pagination (`startTime` + `limit`)
* Limit validation: max 500 bars per request, server defaults to 350
* Automatic backfilling when time range contains fewer bars than requested
**Market Metadata Endpoint:**
* New `getMarketMetadata()` method for comprehensive market information
* Returns 24-hour statistics, latest price, and market initialization data
* Useful for building market overview dashboards
**WebSocket Improvements:**
* Auto-connect flag for authenticated WebSocket channels
* Simplified connection management after authentication
**Vault API Updates:**
* Vault deposit/withdraw methods now use `assetId` (UUID) instead of token addresses
* Consistent with the platform-wide shift to UUID-based asset identification
### Migration Guide
**1. Update Candlestick Calls:**
```typescript theme={null}
// Step 1: Get trading pair (if you only have the symbol)
const pair = await sdk.market.getTradingPairBySymbol("BTC-USDC");
// Step 2: Use the new method signature
const candles = await sdk.market.getCandlesticks(pair.id, "1h", {
startTime: Date.now() - 24 * 60 * 60 * 1000,
endTime: Date.now(),
limit: 350
});
```
**2. For TradingView Integration (backward pagination):**
```typescript theme={null}
// Fetch historical data going backwards from a point in time
const historicalCandles = await sdk.market.getCandlesticks(pair.id, "1h", {
endTime: oldestCandleTime,
limit: 350
});
```
**3. For Real-time Charts (forward pagination):**
```typescript theme={null}
// Fetch latest candles from a starting point
const latestCandles = await sdk.market.getCandlesticks(pair.id, "1h", {
startTime: lastKnownCandleTime,
limit: 100
});
```
### API Reference
**GetCandlesticksParams:**
| Parameter | Type | Required | Description |
| - | - | - | - |
| `startTime` | number | No | Unix timestamp (ms) for range start |
| `endTime` | number | No | Unix timestamp (ms) for range end |
| `limit` | number | No | Max bars to return (default: 350, max: 500) |
**Supported Intervals:**
| Interval | Description |
| - | - |
| `1m` | 1 minute |
| `5m` | 5 minutes |
| `15m` | 15 minutes |
| `1h` | 1 hour |
| `4h` | 4 hours |
| `1d` | 1 day |
***
*For detailed OHLCV documentation, see the [OHLCV & Candlesticks](/ohlcv) guide.*
# V0.5.4
Source: https://docs.0xmonaco.com/releases/v0.5.4
# Monaco Protocol SDK v0.5.4
## Real-Time User Movements & Trade Feed Improvements
This release introduces real-time user movement events via WebSocket and refactors the `useTradeFeed` React hook for better performance and shared state management.
### New Features
#### Real-Time User Movements WebSocket
Track real-time user ledger movements (deposits, withdrawals, trades, fees, and balance changes) with the new movements WebSocket channel.
**Core SDK:**
* **New WebSocket Method**: `sdk.ws.movements(handler)` for subscribing to authenticated balance change events
* **New Event Types**: `UserMovementEvent` and `UserMovementEventData` for ledger movement events
* Track deposits, withdrawals, trades, fees, locks, and unlocks in real-time
**React SDK:**
* **New Hook**: `useUserMovements()` with automatic deduplication and configurable history limit
* Seamless integration with `MonacoProvider` for automatic connection management
* Built-in state management for movement history
**Use Cases:**
* Display real-time balance updates in your UI
* Show live transaction feeds
* Track deposit confirmations instantly
* Monitor trade settlements in real-time
**Example:**
```typescript theme={null}
// TypeScript SDK
const unsubscribe = sdk.ws.movements((event) => {
console.log(`${event.data.transactionType}: ${event.data.amount}`);
console.log(`Balance: ${event.data.balanceBefore} → ${event.data.balanceAfter}`);
});
// React Hook
function MovementsFeed() {
const { movements, loading } = useUserMovements(50); // Optional: max movements (default: 50)
return (
{loading && Loading movements...
}
{movements.map(movement => (
{movement.data.transactionType}: {movement.data.amount}
))}
);
}
```
[Learn more about WebSocket Movements →](/websocket#movement-events)
***
### Breaking Changes
#### `useTradeFeed` Hook Refactored
The `useTradeFeed` React hook has been refactored to share WebSocket subscriptions and state across components, improving performance and eliminating duplicate connections.
**What Changed:**
```typescript theme={null}
// Before (v0.5.3)
const { trades, error, clearError } = useTradeFeed({
tradingPairId: "pair-uuid",
maxTrades: 100, // Custom limit
});
// After (v0.5.4)
const { trades, error } = useTradeFeed({
tradingPairId: "pair-uuid",
// maxTrades removed - fixed at 50 trades
// clearError removed
});
```
**Removed:**
* `maxTrades` parameter (now fixed at 50 trades)
* `clearError` function
**New Features:**
* **Shared Subscriptions**: Multiple components using the same `tradingPairId` now share both the WebSocket connection and trade state
* **Optimized Performance**: Initial trades are fetched only once when the first component subscribes
* **Automatic Cleanup**: Reference-counted subscriptions automatically clean up when the last subscriber unmounts
* **TradeFeedProvider**: Wrapped by `MonacoProvider` to manage shared trade feed subscriptions
**Migration Guide:**
If you were using `maxTrades`:
```typescript theme={null}
// Before
const { trades } = useTradeFeed({ tradingPairId, maxTrades: 100 });
// After - limit to 50 in your component
const { trades } = useTradeFeed({ tradingPairId });
const limitedTrades = trades.slice(0, 50); // Handle limiting in UI
```
If you were using `clearError`:
```typescript theme={null}
// Before
const { error, clearError } = useTradeFeed({ tradingPairId });
// After - handle errors differently
const { error } = useTradeFeed({ tradingPairId });
// Errors are now handled by the shared provider
```
***
### Bug Fixes
**Core SDK:**
* Fixed self-referential import in `useOrderbook` hook
* Fixed circular dependency between provider and hooks modules
* WebSocket now supports multiple handlers per channel, enabling shared subscriptions across consumers
* Subscribe/Unsubscribe messages are only sent on first subscriber and last unsubscriber for each channel
***
### Technical Details
**WebSocket Enhancements:**
* Multiple handlers per channel support
* Improved subscription management
* Better error handling and connection state management
* Reduced message overhead by batching subscribe/unsubscribe operations
**React Provider Architecture:**
* `TradeFeedProvider` automatically wrapped by `MonacoProvider`
* Shared state management across all trade feed consumers
* Reference counting for automatic resource cleanup
* Optimized initial data fetching
***
### Upgrade Instructions
1. **Update your dependencies:**
```bash theme={null}
npm install @0xmonaco/core@0.5.4 @0xmonaco/react@0.5.4
```
2. **Update `useTradeFeed` usage:**
* Remove `maxTrades` parameter
* Remove `clearError` usage
* Handle trade limiting in your UI if needed
3. **Test WebSocket connections:**
* Multiple components with the same `tradingPairId` should share the same connection
* Verify automatic cleanup when components unmount
4. **Explore new movements WebSocket:**
* Try the new `sdk.ws.movements()` method
* Use `useUserMovements()` hook in React applications
***
### API Reference
**New Types:**
* `UserMovementEvent` - WebSocket event structure for movements
* `UserMovementEventData` - Detailed movement data including balances, amounts, and references
**Updated Hooks:**
* `useTradeFeed` - Shared subscription pattern with fixed trade limit
* `useUserMovements` - New hook for real-time ledger movements
**Updated SDK Methods:**
* `sdk.ws.movements(handler)` - Subscribe to user movement events
***
*For complete documentation, see the [WebSocket API reference](/websocket) and [React SDK documentation](/sdk/react/intro).*
# V0.5.5
Source: https://docs.0xmonaco.com/releases/v0.5.5
# Monaco Protocol SDK v0.5.5
## Batch Cancel Orders & Order Status Simplification
This release introduces batch order cancellation for efficient multi-order management and simplifies order statuses by removing deprecated intermediate states.
### New Features
#### Batch Cancel Orders
Cancel multiple orders in a single API call, or cancel all active orders at once.
**Core SDK:**
* **New Method**: `sdk.trading.batchCancelOrders()` for cancelling multiple orders efficiently
* **Cancel All**: Use `{ cancelAll: true }` to cancel all active orders for the user
* **Detailed Results**: Returns individual success/failure status for each order
**Use Cases:**
* Emergency position exit (cancel all orders instantly)
* Bulk order management for market makers
* Portfolio rebalancing with clean slate
* Efficient cleanup of stale orders
***
#### Real-time Order Tracking
Track all your orders in real-time with the new `useUserOrders` React hook. Automatically syncs with WebSocket for instant order updates.
**React SDK:**
* **New Hook**: `useUserOrders()` for real-time order tracking
* **Automatic Sync**: Fetches initial orders via REST, then subscribes to WebSocket updates
* **Smart Deduplication**: Merges REST and WebSocket data without duplicates
* **Manual Refresh**: Built-in `refresh()` function for on-demand updates
**Features:**
* Real-time order status updates (placed, filled, cancelled, etc.)
* Automatic state management with configurable history limit
* Authentication-aware (requires valid JWT token)
* Built-in error handling and retry support
**Example:**
```typescript theme={null}
import { useUserOrders } from "@0xmonaco/react";
function OrdersPanel() {
const { orders, loading, subscribed } = useUserOrders(100);
// Filter active orders
const activeOrders = orders.filter(
o => o.status === "SUBMITTED" || o.status === "PARTIALLY_FILLED"
);
return (
Live: {subscribed ? "Connected" : "Disconnected"}
Active Orders ({activeOrders.length})
{activeOrders.map(order => (
{order.trading_pair}: {order.side} {order.quantity}
{order.filled_quantity !== "0" && (
Filled: {order.filled_quantity}
)}
))}
);
}
```
[Learn more about useUserOrders →](/sdk/react/hooks/use-user-orders)
**Example:**
```typescript theme={null}
// Cancel specific orders
const result = await sdk.trading.batchCancelOrders({
orderIds: ['order_123', 'order_456', 'order_789']
});
console.log(`Cancelled ${result.total_cancelled} of ${result.total_requested} orders`);
// Check individual results
result.results.forEach(r => {
if (r.success) {
console.log(`Order ${r.order_id} cancelled at ${r.cancelled_at}`);
} else {
console.log(`Order ${r.order_id} failed: ${r.error?.message}`);
}
});
// Cancel ALL active orders
const allCancelled = await sdk.trading.batchCancelOrders({ cancelAll: true });
console.log(`Emergency exit: cancelled ${allCancelled.total_cancelled} orders`);
```
[Learn more about batch cancel →](/developers/trade)
***
### Breaking Changes
#### Order Status Simplification
The `PENDING` and `ACKNOWLEDGED` order statuses have been removed from the API. Orders now transition directly to `SUBMITTED` status.
**What Changed:**
```typescript theme={null}
// Before (v0.5.4)
type OrderStatus =
| "PENDING" // ❌ Removed
| "ACKNOWLEDGED" // ❌ Removed
| "SUBMITTED"
| "PARTIALLY_FILLED"
| "FILLED"
| "CANCELLED"
| "REJECTED"
| "EXPIRED"
| "SETTLED";
// After (v0.5.5)
type OrderStatus =
| "SUBMITTED" // Orders start here
| "PARTIALLY_FILLED"
| "FILLED"
| "CANCELLED"
| "REJECTED"
| "EXPIRED"
| "SETTLED";
```
**Migration Guide:**
If you were filtering or checking for `PENDING` or `ACKNOWLEDGED` statuses, update your code:
```typescript theme={null}
// Before
if (order.status === 'PENDING' || order.status === 'ACKNOWLEDGED') {
// Wait for order to be processed
}
// After
if (order.status === 'SUBMITTED') {
// Order is active and waiting to be filled
}
```
**Why this change:**
* Simplifies order lifecycle
* Reduces intermediate states that caused confusion
* Improves consistency across REST and WebSocket APIs
* Orders now have a cleaner state machine
***
### Breaking Change: vault.getBalance() Deprecated
The `vault.getBalance()` method has been deprecated and no longer functions. Use the Profile API for balance queries instead.
**What Changed:**
```typescript theme={null}
// Before (v0.5.4 and earlier)
const balance = await sdk.vault.getBalance(assetId);
console.log('Balance:', balance.formatted);
// After (v0.5.5+)
const balance = await sdk.profile.getUserBalanceByAssetId(assetId);
console.log('Available:', balance.available_balance);
console.log('Locked:', balance.locked_balance);
console.log('Total:', balance.total_balance);
```
**Why this change:**
* Profile API provides more detailed balance information (available, locked, total)
* Consolidates balance queries with other profile data
* Improves performance with backend optimizations
* Simplifies SDK architecture by removing duplicate functionality
**Migration Guide:**
Replace `sdk.vault.getBalance(assetId)` with `sdk.profile.getUserBalanceByAssetId(assetId)`:
```typescript theme={null}
// Old approach
const vaultBalance = await sdk.vault.getBalance(assetId);
console.log(`Balance: ${vaultBalance.formatted}`);
// New approach
const profileBalance = await sdk.profile.getUserBalanceByAssetId(assetId);
console.log(`Available: ${profileBalance.available_balance}`);
console.log(`Locked in orders: ${profileBalance.locked_balance}`);
console.log(`Total: ${profileBalance.total_balance}`);
```
For getting all balances:
```typescript theme={null}
const { balances } = await sdk.profile.getUserBalances({ limit: 100 });
balances.forEach(b => {
console.log(`${b.symbol}: ${b.total_balance}`);
});
```
[Learn more about Profile API →](/sdk/typescript/profile)
***
### Breaking Change: TypeScript Type Updates
Several TypeScript interfaces have been updated for better consistency and to expose raw balance fields. These are breaking changes if you're using TypeScript.
#### `GetUserBalancesResponse`: Pagination Field Renamed
**What Changed:**
```typescript theme={null}
// Before (v0.5.4)
interface GetUserBalancesResponse {
balances: AccountBalance[];
total: number; // Old field name
limit: number;
offset: number;
}
// After (v0.5.5+)
interface GetUserBalancesResponse {
balances: AccountBalance[];
total_count: number; // Renamed for consistency
limit: number;
offset: number;
}
```
**Migration:**
```typescript theme={null}
const response = await sdk.profile.getUserBalances();
console.log(`Total: ${response.total_count}`); // Changed from response.total
```
#### `AccountBalance`: New Required Fields
**What Changed:**
```typescript theme={null}
// After (v0.5.5+)
interface AccountBalance {
asset_id: string; // NEW: Asset UUID
token: string;
symbol: string | null;
decimals: number;
available_balance: string;
locked_balance: string;
total_balance: string;
available_balance_raw: string; // NEW: Raw balance (wei)
locked_balance_raw: string; // NEW: Raw balance (wei)
}
```
**New fields:**
* `asset_id` - Asset UUID for API operations
* `available_balance_raw` - Available balance in raw format (wei/smallest unit)
* `locked_balance_raw` - Locked balance in raw format (wei/smallest unit)
**Why this change:**
* Enables precise balance calculations without floating-point errors
* Provides asset\_id for direct use in deposit/withdraw operations
* Matches backend API response structure
#### `LedgerMovement`: Extended Metadata
**What Changed:**
The `LedgerMovement` interface has been extended with additional fields:
```typescript theme={null}
interface LedgerMovement {
// ... existing fields ...
amount_raw: string; // NEW: Raw amount
symbol: string | null; // Now explicitly nullable
decimals: number;
balance_before_raw: string | null; // NEW
balance_after_raw: string | null; // NEW
locked_before: string | null; // NEW
locked_before_raw: string | null; // NEW
locked_after: string | null; // NEW
locked_after_raw: string | null; // NEW
reference_id: string | null; // NEW
reference_type: string | null; // NEW
tx_hash: string | null; // NEW
block_number: number | null; // NEW
}
```
**Why this change:**
* More accurate representation of on-ledger movements
* Aligns with WebSocket event payloads
* Provides raw values for precise calculations
#### `MarketMetadata`: Nullable Fields
**What Changed:**
`MarketMetadata` fields are now nullable to reflect API behavior for markets without trade history:
```typescript theme={null}
interface MarketMetadata {
// These fields are null if no trades have occurred yet
last_price: string | null;
last_price_timestamp: number | null;
// These fields are null if less than 24 hours of data
high_24h: string | null;
low_24h: string | null;
volume_24h: string | null;
price_change_24h: string | null;
price_change_percent_24h: string | null;
// Null if not available
market_initialization_timestamp: number | null;
}
```
**Migration:**
Add null checks when accessing `MarketMetadata` fields:
```typescript theme={null}
const metadata = await sdk.market.getMarketMetadata(tradingPairId);
// Before (v0.5.4) - assumed values always present
const price = metadata.last_price;
// After (v0.5.5) - handle null values
const price = metadata.last_price ?? 'N/A';
if (metadata.last_price !== null) {
console.log(`Last price: ${metadata.last_price}`);
} else {
console.log('No trades yet');
}
```
**Why this change:**
* Accurately represents markets that haven't had any trades yet
* Prevents displaying misleading zero or undefined values
* Matches actual API response behavior
***
### API Reference
**New Endpoint:**
* `POST /api/v1/orders/batch-cancel` - Cancel multiple orders in a single request
**New Types:**
```typescript theme={null}
interface BatchCancelOrdersResponse {
success: boolean; // Overall operation success
total_requested: number; // Total orders requested to cancel
total_cancelled: number; // Total orders successfully cancelled
total_failed: number; // Total orders that failed to cancel
results: BatchCancelResult[]; // Individual results for each order
}
interface BatchCancelResult {
order_id: string; // Order identifier
success: boolean; // Whether cancellation was successful
cancelled_at?: string; // ISO 8601 timestamp when cancelled
error?: { // Error details if failed
code: string;
message: string;
};
}
```
**Query Parameters:**
* `cancel_all` (boolean) - Set to `true` to cancel all active orders
***
### Upgrade Instructions
1. **Update your dependencies:**
```bash theme={null}
npm install @0xmonaco/core@0.5.5 @0xmonaco/react@0.5.5
```
2. **Update balance queries:**
* Replace `sdk.vault.getBalance(assetId)` with `sdk.profile.getUserBalanceByAssetId(assetId)`
* Update response handling to use new balance structure (available\_balance, locked\_balance, total\_balance)
* See [Account Vault](/developers/vault) for examples
3. **Update order status checks:**
* Remove checks for `PENDING` status
* Remove checks for `ACKNOWLEDGED` status
* Use `SUBMITTED` as the initial active order state
4. **Consider using batch cancel:**
* Replace loops calling `cancelOrder()` with `batchCancelOrders()`
* Use `cancelAll: true` for emergency exits
5. **Handle nullable MarketMetadata fields:**
* Add null checks when accessing `last_price`, `high_24h`, `low_24h`, etc.
* Markets without trade history will return `null` for these fields
6. **Test your flows:**
* Verify balance queries work correctly
* Verify order status transitions work correctly
* Test batch cancellation with multiple orders
* Test handling of new markets with no trade history
***
*For complete documentation, see [Order Management](/developers/trade).*
# V0.5.6
Source: https://docs.0xmonaco.com/releases/v0.5.6
# Monaco Protocol SDK v0.5.6
## Real-Time Balance Updates & API Improvements
This release introduces real-time balance tracking via WebSocket, refines the batch cancel API with two dedicated methods for clearer usage, and improves error handling for vault operations.
***
## New Features
### Real-Time Balance Updates via WebSocket
Track your account balances in real-time with the new WebSocket balance channel and React hook.
**Core SDK:**
* **New Method**: `sdk.ws.balances()` for subscribing to live balance changes
* **Authenticated Channel**: Requires valid JWT token (user-specific updates)
* **Detailed Events**: Includes available, locked, and total balances (both normalized and raw values)
* **Update Reasons**: Know why balances changed (lock, unlock, deposit, withdrawal, trade)
**React SDK:**
* **New Hook**: `useUserBalances()` for real-time balance tracking
* **Hybrid Approach**: Fetches initial balances via REST, then subscribes to WebSocket for live updates
* **Manual Refresh**: Built-in `refresh()` function for on-demand balance reload
* **Connection Status**: Track if WebSocket subscription is active
**Use Cases:**
* Display live balance updates during trading
* Show real-time locked balance as orders execute
* Monitor deposits and withdrawals instantly
* Track fee deductions from trades
* Update balance UI without polling
**Example: WebSocket Balance Subscription**
```typescript theme={null}
import { MonacoSDK } from "@0xmonaco/core";
// Authenticate first
await sdk.login('your-client-id');
// Subscribe to balance updates
const unsubscribe = sdk.ws.balances((event) => {
console.log('Balance updated:', event.data.reason);
console.log('Token:', event.data.tokenSymbol);
console.log('Available:', event.data.available);
console.log('Locked:', event.data.locked);
console.log('Total:', event.data.total);
// Update UI with new balances
updateBalanceDisplay(event.data);
});
// Later, to unsubscribe:
unsubscribe();
```
**Example: React Hook**
```tsx theme={null}
import { useUserBalances } from "@0xmonaco/react";
function BalanceDashboard() {
const { balances, loading, subscribed, error, refresh } = useUserBalances();
if (loading) return Loading balances...;
if (error) return Error: {error.message};
return (
Your Balances
{subscribed && ● Live}
{balances.map(balance => (
{balance.symbol}
Available: {balance.available_balance}
Locked: {balance.locked_balance}
Total: {balance.total_balance}
))}
);
}
```
**Event Structure:**
```typescript theme={null}
interface UserBalanceEvent {
eventType: "balance_update";
userId: string;
data: {
tokenAddress: string; // Token contract address
tokenSymbol: string | null; // Token symbol (e.g., "USDC")
available: string; // Available balance (normalized)
availableRaw: string; // Available balance (raw/wei)
locked: string; // Locked in orders (normalized)
lockedRaw: string; // Locked in orders (raw/wei)
total: string; // Total balance (normalized)
totalRaw: string; // Total balance (raw/wei)
reason: "lock" | "unlock" | "deposit" | "withdrawal" | "trade";
referenceId: string | null; // Related order/movement ID
updatedAt: string; // ISO 8601 timestamp
};
}
```
[Learn more about WebSocket API →](/websocket)
***
### Batch Cancel API Split
The `batchCancelOrders()` method has been split into two dedicated methods for clearer intent and simpler usage.
**What Changed:**
```typescript theme={null}
// Before (v0.5.5)
// Cancel specific orders
await sdk.trading.batchCancelOrders({
orderIds: ['order-1', 'order-2', 'order-3']
});
// Cancel all orders
await sdk.trading.batchCancelOrders({ cancelAll: true });
// After (v0.5.6)
// Cancel specific orders - dedicated method
await sdk.trading.batchCancel(['order-1', 'order-2', 'order-3']);
// Cancel all orders - dedicated method
await sdk.trading.batchCancelAll();
// NEW: Cancel all orders for a specific trading pair
await sdk.trading.batchCancelAll(tradingPairId);
```
**Core SDK:**
* **New Method**: `batchCancel(orderIds: string[])` - Cancel specific orders by ID
* **New Method**: `batchCancelAll(tradingPairId?: string)` - Cancel all active orders, optionally filtered by trading pair
* **Clearer Intent**: Method names explicitly indicate what they do
* **Simpler Parameters**: No more object wrappers - just pass the array or trading pair ID
**React SDK:**
* Both methods available in `useTrade()` hook
* Same signatures as Core SDK
**Example:**
```typescript theme={null}
// Cancel specific orders
const result = await sdk.trading.batchCancel([
'order-id-1',
'order-id-2',
'order-id-3'
]);
console.log(`Cancelled: ${result.total_cancelled}/${result.total_requested}`);
// Check individual results
result.results.forEach(r => {
if (r.success) {
console.log(`Order ${r.order_id} cancelled`);
} else {
console.log(`Order ${r.order_id} failed: ${r.error.message}`);
}
});
// Cancel ALL active orders globally
const allResult = await sdk.trading.batchCancelAll();
console.log(`Emergency exit: cancelled ${allResult.total_cancelled} orders`);
// Cancel all orders for a specific trading pair
const pair = await sdk.market.getTradingPairBySymbol('ETH/USDC');
const pairResult = await sdk.trading.batchCancelAll(pair.id);
console.log(`Cancelled ${pairResult.total_cancelled} ETH/USDC orders`);
```
**Migration Guide:**
Replace `batchCancelOrders()` calls with the appropriate new method:
```typescript theme={null}
// Old: Cancel specific orders
await sdk.trading.batchCancelOrders({ orderIds: [id1, id2, id3] });
// New: Use batchCancel with direct array
await sdk.trading.batchCancel([id1, id2, id3]);
// Old: Cancel all orders
await sdk.trading.batchCancelOrders({ cancelAll: true });
// New: Use batchCancelAll (no parameters)
await sdk.trading.batchCancelAll();
```
**Why this change:**
* **Clearer API**: Method names explicitly describe the action
* **Simpler Usage**: No need to wrap parameters in objects
* **Better TypeScript**: Type inference works better with direct parameters
* **Trading Pair Filter**: New capability to cancel all orders for a specific pair
[Learn more about trading API →](/sdk/typescript/trades)
***
## Technical Updates
### viem Peer Dependency
The `viem` package has been moved from a direct dependency to a peer dependency (version `^2.45.2`) in both `@0xmonaco/core` and `@0xmonaco/types` packages.
**What this means:**
* Your application must now install `viem` directly
* Prevents type duplication issues when consuming the SDK
* Ensures consistent `viem` version across your entire application
**Installation:**
```bash theme={null}
# Install viem alongside Monaco SDK
npm install @0xmonaco/core@0.5.6 viem@^2.45.2
# Or for React
npm install @0xmonaco/react@0.5.6 @0xmonaco/core@0.5.6 viem@^2.45.2
```
### React Hook Count Correction
Monaco Protocol React SDK now has **14 hooks** (not 13 as previously documented):
1. `useAuth` - Authentication
2. `useFees` - Fee simulation
3. `useMarket` - Market data
4. `useMonacoSDK` - SDK instance access
5. `useOHLCV` - Real-time candlesticks
6. `useOrderbook` - Real-time orderbook
7. `useProfile` - User profile
8. `useTokenLifecycle` - Token management
9. `useTrade` - Trading operations
10. `useTradeFeed` - Real-time trades
11. **`useUserBalances`** - Real-time balances (NEW in v0.5.6)
12. `useUserMovements` - Account movements
13. `useUserOrders` - Real-time orders
14. `useVault` - Vault operations
***
## Bug Fixes
### Proto-Generated API Contract Alignment
This release also fixes several mismatches between the proto-generated API surface and the live service behavior.
**What Changed:**
* Implemented gRPC market metadata so the published contract no longer points to an unimplemented endpoint
* Defined gRPC batch order operations for batch create, batch replace, batch cancel, and cancel-all flows in the published contract; server-side handlers remain stubbed and return `UNIMPLEMENTED` in this release
* Aligned gRPC market pair filtering with the HTTP API by treating `base_token` and `quote_token` as token symbols instead of contract addresses
* Removed transport-only `source` fields from account balance and movement responses so generated contracts match the HTTP API
* Relaxed generated order `status` query constraints so comma-separated filters like `FILLED,PARTIALLY_FILLED` remain valid
* Replaced a panic path in `get_balance_by_asset` with proper `invalid_argument` validation for malformed asset UUIDs
### Vault Operations: Clear Error When No Account Available
Vault operations now throw a clear `InvalidConfigError` when the wallet client has no account connected, instead of failing with an unclear error message later in the transaction flow.
**What Changed:**
Previously, if you initialized the SDK with a wallet client but no account was connected, vault operations would fail with unclear errors during transaction execution. Now, you'll receive an immediate, actionable error.
```typescript theme={null}
import { MonacoSDK, InvalidConfigError } from "@0xmonaco/core";
import { createWalletClient, http } from "viem";
import { seiTestnet } from "viem/chains";
// Wallet client without account
const walletClient = createWalletClient({
chain: seiTestnet,
transport: http()
// No account specified
});
const sdk = new MonacoSDK({
walletClient,
network: "staging",
seiRpcUrl: "https://evm-rpc-testnet.sei-apis.com"
});
try {
// v0.5.6+: Clear error immediately
await sdk.vault.deposit(assetId, 1000000000n);
} catch (error) {
if (error instanceof InvalidConfigError && error.field === "account") {
console.error("No account available in wallet client");
console.error("Suggestion:", error.suggestion);
// Handle: prompt user to connect wallet or provide account
}
}
```
**When This Error Occurs:**
This error is thrown when calling any vault operation if:
* Wallet client is provided to SDK but `account` property is `undefined` or `null`
* User has disconnected their wallet after SDK initialization
* Wallet client was created without specifying an account
**Affected Operations:**
* `sdk.vault.deposit()`
* `sdk.vault.withdraw()`
* `sdk.vault.approve()`
* `sdk.vault.getAllowance()`
* `sdk.vault.needsApproval()`
**Error Details:**
```typescript theme={null}
InvalidConfigError: "No account available in wallet client"
field: "account"
code: "INVALID_CONFIG"
suggestion: "Ensure wallet is connected and account is available"
```
**Why This Change:**
* **Earlier Detection**: Catch configuration issues before attempting blockchain transactions
* **Clearer Errors**: Specific error message indicates exactly what's wrong
* **Better DX**: Developers can handle this case explicitly and provide better UX
* **Actionable Suggestions**: Error includes suggestion on how to fix the issue
**Error Handling Example:**
```typescript theme={null}
try {
await sdk.vault.deposit(assetId, amount);
} catch (error) {
if (error instanceof InvalidConfigError) {
// New in v0.5.6: Handle wallet configuration errors
if (error.field === "account") {
alert("Please connect your wallet to continue");
}
} else if (error instanceof APIError) {
// Existing error handling
console.error("API error:", error.message);
}
}
```
***
## Internal Improvements
This release includes internal code quality improvements:
* Added TypeScript `override` keywords to error class methods for better type safety
* Improved null safety checks in React hooks (`useOHLCV`, `useUserOrders`, `useUserBalances`)
* Simplified network URL resolution logic
* Updated build tooling and linting configuration
These changes have no impact on the public API or usage patterns.
***
## Upgrade Instructions
### Install New Versions
```bash theme={null}
# NPM
npm install @0xmonaco/core@0.5.6 @0xmonaco/types@0.5.6 @0xmonaco/react@0.5.6 viem@^2.45.2
# Bun
bun add @0xmonaco/core@0.5.6 @0xmonaco/types@0.5.6 @0xmonaco/react@0.5.6 viem@^2.45.2
```
### Migration Steps
#### 1. Ensure viem is installed
```bash theme={null}
# Check if viem is already installed
npm list viem
# If not, install it
npm install viem@^2.45.2
```
#### 2. Update batch cancel calls
Find all occurrences of `batchCancelOrders()` and replace with the appropriate method:
```typescript theme={null}
// Replace specific order cancellations
- await sdk.trading.batchCancelOrders({ orderIds: [id1, id2] });
+ await sdk.trading.batchCancel([id1, id2]);
// Replace cancel-all calls
- await sdk.trading.batchCancelOrders({ cancelAll: true });
+ await sdk.trading.batchCancelAll();
```
#### 3. (Optional) Add balance tracking
Take advantage of the new real-time balance updates:
```typescript theme={null}
// TypeScript SDK
const unsubscribe = sdk.ws.balances((event) => {
updateBalanceUI(event.data);
});
// React SDK
import { useUserBalances } from "@0xmonaco/react";
function Component() {
const { balances, subscribed } = useUserBalances();
// Your component logic
}
```
#### 4. (Optional) Improve vault error handling
Add explicit error handling for vault operations:
```typescript theme={null}
import { InvalidConfigError } from "@0xmonaco/core";
try {
await sdk.vault.deposit(assetId, amount);
} catch (error) {
if (error instanceof InvalidConfigError && error.field === "account") {
// Prompt user to connect wallet
alert("Please connect your wallet to continue");
}
}
```
### Testing Recommendations
1. **Test batch cancel migrations**: Verify updated calls work correctly
2. **Test viem dependency**: Ensure your app builds with the peer dependency
3. **Test balance updates**: Verify WebSocket balance subscriptions work in your environment
4. **Test vault error handling**: Confirm improved error messages display correctly
***
*For complete documentation, see the [WebSocket API guide](/websocket), [Trading guide](/sdk/typescript/trades), [Vault Operations guide](/sdk/typescript/vault), and [Error Handling guide](/sdk/typescript/error-handling).*
# V0.5.7
Source: https://docs.0xmonaco.com/releases/v0.5.7
# Monaco Protocol SDK v0.5.7
## Batch Orders, MCP Server & Network Configuration Improvements
This release introduces batch order creation and replacement for high-frequency trading workflows, a fully rebuilt MCP server for AI assistant integrations, removes the manual WebSocket URL configuration requirement, and tightens the `network` parameter to prevent misconfiguration.
***
## New Features
### Batch Create Orders
Submit multiple orders in a single API call with the new `batchCreate()` method.
**Core SDK:**
* **New Method**: `sdk.trading.batchCreate(orders: BatchCreateOrderParams[])` — `POST /api/v1/orders/batch-create`
* **Partial Success**: Each order is processed independently — failures don't block the rest
* **Per-Order Results**: Detailed outcome per order including match result and error details if failed
* **Full Order Support**: Supports both LIMIT and MARKET orders; time-in-force options (GTC, IOC, FOK) apply to LIMIT orders only
**React SDK:**
* **New Hook Method**: `batchCreate()` available in `useTrade()`
**Example: Core SDK**
```typescript theme={null}
import { MonacoSDK } from "@0xmonaco/core";
const pair = await sdk.market.getTradingPairBySymbol("ETH/USDC");
const result = await sdk.trading.batchCreate([
{
tradingPairId: pair.id,
orderType: "LIMIT",
side: "BUY",
quantity: "1.0",
price: "2000.00",
timeInForce: "GTC",
},
{
tradingPairId: pair.id,
orderType: "LIMIT",
side: "BUY",
quantity: "0.5",
price: "1950.00",
timeInForce: "GTC",
},
]);
console.log(`Created ${result.total_succeeded} of ${result.total_requested} orders`);
result.results.forEach((r) => {
if (r.success) {
console.log(`Order ${r.order_id} created`);
} else {
console.error(`Failed: ${r.error?.code} — ${r.error?.message}`);
}
});
```
**Example: React Hook**
```tsx theme={null}
import { useTrade } from "@0xmonaco/react";
function TradingPanel({ pairId }: { pairId: string }) {
const { batchCreate } = useTrade();
async function placeBracketOrders() {
const result = await batchCreate([
{ tradingPairId: pairId, orderType: "LIMIT", side: "BUY", quantity: "1.0", price: "2000.00", timeInForce: "GTC" },
{ tradingPairId: pairId, orderType: "LIMIT", side: "BUY", quantity: "0.5", price: "1950.00", timeInForce: "GTC" },
]);
console.log(`Created ${result.total_succeeded} orders`);
}
}
```
**New Types:**
* `BatchCreateOrderParams` — Per-order input (tradingPairId, orderType, side, quantity, price?, timeInForce?, slippageTolerance?, expirationDate?, useMasterBalance?)
* `BatchCreateOrdersResponse` — `{ success, total_requested, total_succeeded, total_failed, results }`
* `BatchCreateResult` — `{ order_id, success, match_result?, error? }`
* `BatchError` — `{ code, message }`
***
### Batch Replace Orders
Replace multiple orders atomically in a single API call with the new `batchReplace()` method.
**Core SDK:**
* **New Method**: `sdk.trading.batchReplace(orders: BatchReplaceOrderParams[])` — `POST /api/v1/orders/batch-replace`
* **Atomic Per-Order**: Each replacement cancels the original and creates a new order independently
* **New Order IDs**: Each successful replacement returns a new order ID
* **Flexible Updates**: `price` and `quantity` are both optional — omit either to keep the original value
**React SDK:**
* **New Hook Method**: `batchReplace()` available in `useTrade()`
**Example: Core SDK**
```typescript theme={null}
const result = await sdk.trading.batchReplace([
{ orderId: "order_1", price: "2050.00" },
{ orderId: "order_2", price: "1975.00", quantity: "2.0" },
{ orderId: "order_3", price: "1925.00" },
]);
console.log(`Replaced ${result.total_succeeded} of ${result.total_requested} orders`);
result.results.forEach((r) => {
if (r.success) {
console.log(`New order ID: ${r.new_order_id} (replaced ${r.original_order_id})`);
} else {
console.error(`Failed: ${r.error?.code} — ${r.error?.message}`);
}
});
```
**Example: React Hook**
```tsx theme={null}
import { useTrade } from "@0xmonaco/react";
function OrderGrid() {
const { batchReplace } = useTrade();
async function repriceAll(orders: { id: string; newPrice: string }[]) {
const result = await batchReplace(orders.map(({ id, newPrice }) => ({ orderId: id, price: newPrice })));
console.log(`Replaced ${result.total_succeeded} orders`);
}
}
```
**New Types:**
* `BatchReplaceOrderParams` — `{ orderId, price?, quantity?, useMasterBalance? }`
* `BatchReplaceOrdersResponse` — `{ success, total_requested, total_succeeded, total_failed, results }`
* `BatchReplaceResult` — `{ original_order_id, success, new_order_id?, updated_fields?, match_result?, error? }`
***
### Movements Filters
Server-side filtering is now available on the user movements endpoint.
**Core SDK — `getPaginatedUserMovements()`:**
* New parameter: `entry_type` — filter by entry type
* New parameter: `transaction_type` — filter by transaction type
* New parameter: `asset_id` — filter by asset UUID
**New types** (`@0xmonaco/types`):
* `LedgerEntryType` — union of entry type values
* `TransactionType` — union of transaction type values
**MCP server — `get_movements` tool:**
* Now accepts `entry_type`, `transaction_type`, and `asset_id` as optional filter inputs
```typescript theme={null}
// Core SDK
const deposits = await sdk.profile.getPaginatedUserMovements({
entry_type: "CREDIT",
transaction_type: "DEPOSIT",
});
const assetMovements = await sdk.profile.getPaginatedUserMovements({
asset_id: "123e4567-e89b-12d3-a456-426614174000",
});
```
```tsx theme={null}
// React hook
const { movements } = useUserMovements({ transaction_type: "TRADE" });
```
***
### MCP Server
The Monaco MCP server has been rebuilt from the ground up. It exposes Monaco's full API surface as tools that any MCP-compatible AI assistant (Claude, Cursor, Windsurf) can call directly — no SDK code required.
**What's new in v0.5.7:**
* **26 tools across 9 modules**: search, market, orderbook, trades, trading, profile, fees, vault, and faucet
* **Public-only mode**: run with zero configuration to get read-only market data instantly — `MONACO_PRIVATE_KEY` and `MONACO_CLIENT_ID` are optional
* **Auto RPC defaults**: `MONACO_RPC_URL` defaults per network — no manual URL configuration needed
* **Node 16 compatibility**: polyfills for `fetch` and `WebSocket` for environments like Claude Desktop that run older Node versions
For setup instructions and a full list of available tools, see [MCP Server](/sdk/mcp-server).
***
## Breaking Changes
### `useUserMovements` Now Accepts an Options Object
The `useUserMovements` hook signature has changed from a positional `maxMovements` number to an options object.
```tsx theme={null}
// Before (v0.5.6)
const { movements } = useUserMovements(50);
// After (v0.5.7)
const { movements } = useUserMovements({ maxMovements: 50 });
```
All previous options are available under the same names in the options object. The filter params (`entry_type`, `transaction_type`, `asset_id`) are also passed here.
***
### `wsUrl` Removed from `SDKConfig` and `MonacoProvider`
The `wsUrl` configuration option has been removed. The WebSocket URL is now automatically resolved from the `network` parameter — no manual configuration required.
**Core SDK — What Changed:**
```typescript theme={null}
// Before (v0.5.6)
const sdk = new MonacoSDK({
walletClient,
network: "staging",
seiRpcUrl: "https://evm-rpc-testnet.sei-apis.com",
wsUrl: "wss://staging.apimonaco.xyz/ws", // ← Remove this
});
// After (v0.5.7)
const sdk = new MonacoSDK({
walletClient,
network: "staging",
seiRpcUrl: "https://evm-rpc-testnet.sei-apis.com",
});
```
**React SDK — What Changed:**
```tsx theme={null}
// Before (v0.5.6)
// After (v0.5.7)
```
**Why this change:**
* Eliminates a common source of misconfiguration
* WebSocket and API URLs are always consistent with each other
* Simpler SDK initialization
***
### `Network` Type Now Accepts Only Preset Values
The `network` parameter no longer accepts arbitrary URL strings. Only preset network names are valid. Passing an unsupported value throws `InvalidConfigError`.
**What Changed:**
```typescript theme={null}
// Before (v0.5.6) — custom URL strings were silently accepted
const sdk = new MonacoSDK({
network: "https://my-custom-api.com", // ← No longer works
// ...
});
// After (v0.5.7) — throws InvalidConfigError for unrecognised values
const sdk = new MonacoSDK({
network: "staging", // ← Use a preset name
// ...
});
```
***
## Upgrade Instructions
### Install New Versions
```bash theme={null}
# npm
npm install @0xmonaco/core@0.5.7 @0xmonaco/types@0.5.7 @0xmonaco/react@0.5.7
# Bun
bun add @0xmonaco/core@0.5.7 @0xmonaco/types@0.5.7 @0xmonaco/react@0.5.7
```
### Migration Steps
#### 1. Update `useUserMovements` call sites
```tsx theme={null}
// Before
const { movements } = useUserMovements(50);
// After
const { movements } = useUserMovements({ maxMovements: 50 });
```
#### 2. Remove `wsUrl` from SDK initialization
Find all `MonacoSDK` constructor calls and remove the `wsUrl` property:
```typescript theme={null}
// Before
const sdk = new MonacoSDK({ walletClient, network: "staging", seiRpcUrl: "...", wsUrl: "..." });
// After
const sdk = new MonacoSDK({ walletClient, network: "staging", seiRpcUrl: "..." });
```
#### 3. Remove `wsUrl` from `MonacoProvider`
```tsx theme={null}
// Before
// After
```
#### 4. Remove `wsUrl` from environment variables
If you had `VITE_MONACO_WS_URL` or similar env vars, they are no longer needed.
#### 5. (Optional) Adopt batch order methods
Take advantage of the new batch APIs for high-frequency workflows:
```typescript theme={null}
// Instead of sequential placements
for (const order of orders) {
await sdk.trading.placeLimitOrder(order.pairId, order.side, order.quantity, order.price);
}
// Use batch create
await sdk.trading.batchCreate(orders.map(o => ({
tradingPairId: o.pairId,
orderType: "LIMIT",
side: o.side,
quantity: o.quantity,
price: o.price,
timeInForce: "GTC",
})));
```
***
*For complete documentation, see [Order Management](/developers/trade), [useTrade hook](/sdk/react/hooks/use-trade), and [API Reference](/api-reference/orders/batch-create-orders).*
# V0.5.8
Source: https://docs.0xmonaco.com/releases/v0.5.8
# Monaco Protocol SDK v0.5.8
## Profile Endpoint Simplification & Market Order Cleanup
This release removes extraneous fields from the `/me` profile endpoint and eliminates `timeInForce` from market orders, making both the API and SDK surface more predictable.
***
## Breaking Changes
### `/me` No Longer Returns Balances, Movements, or Orders
`GET /api/v1/accounts/me` now returns only core profile metadata. The `balances`, `recent_movements`, and `recent_orders` fields have been removed from the response.
**Before (v0.5.7):**
```json theme={null}
{
"id": "...",
"address": "0x...",
"username": "trader123",
"account_type": "master",
"can_withdraw": true,
"created_at": "...",
"balances": [...],
"recent_movements": [...],
"recent_orders": [...]
}
```
**After (v0.5.8):**
```json theme={null}
{
"id": "...",
"address": "0x...",
"username": "trader123",
"account_type": "master",
"can_withdraw": true,
"created_at": "..."
}
```
Use the dedicated endpoints for account data:
| Data | Endpoint |
| - | - |
| Balances | `GET /api/v1/accounts/balances` |
| Single asset balance | `GET /api/v1/accounts/balances/{asset_id}` |
| Transaction history | `GET /api/v1/accounts/movements` |
| Orders | `GET /api/v1/orders` |
**Migration — Core SDK:**
```typescript theme={null}
// Before
const profile = await sdk.profile.getProfile();
const balances = profile.balances;
const movements = profile.recent_movements;
// After
const profile = await sdk.profile.getProfile();
const { balances } = await sdk.profile.getUserBalances({ limit: 20, offset: 0 });
const movements = await sdk.profile.getPaginatedUserMovements({ page: 1, limit: 20 });
const orders = await sdk.trading.getPaginatedOrders({ page: 1, page_size: 20 });
```
**Migration — React SDK:**
`useProfile()` now fetches balances, movements, and orders automatically when `fetchProfile()` is called, and exposes them as top-level state. No manual migration required for most consumers.
```tsx theme={null}
const {
profile,
balances, // AccountBalance[] — populated by fetchProfile()
movements, // GetPaginatedUserMovementsResponse | undefined
orders, // GetPaginatedOrdersResponse | undefined
loading,
fetchProfile,
getPaginatedOrders, // new — fetches orders and updates state
} = useProfile();
```
***
### `timeInForce` Removed from Market Orders
`timeInForce` is no longer accepted for market orders anywhere in the SDK. The API already rejects market orders that include `time_in_force` with a 400 error.
**Affected packages:** `@0xmonaco/types`, `@0xmonaco/core`, `@0xmonaco/react`
`timeInForce` remains fully supported on limit orders and batch order placement.
**Migration:**
```typescript theme={null}
// Before — timeInForce was silently ignored or caused an error
await sdk.trading.placeMarketOrder(pairId, "BUY", "0.5", {
timeInForce: "IOC", // remove this
});
// After
await sdk.trading.placeMarketOrder(pairId, "BUY", "0.5", {
slippageTolerance: 0.01, // optional, 1% max slippage
});
```
***
## Package Versions
| Package | Version |
| - | - |
| `@0xmonaco/core` | 0.5.8 |
| `@0xmonaco/react` | 0.5.8 |
| `@0xmonaco/types` | 0.5.8 |
| `@0xmonaco/mcp-server` | 0.5.8 |
| `@0xmonaco/contracts` | 0.5.8 |
```bash theme={null}
npm install @0xmonaco/core@0.5.8 @0xmonaco/react@0.5.8
```
All examples in these docs target the **staging** network (`https://staging.apimonaco.xyz`).
# V0.5.9
Source: https://docs.0xmonaco.com/releases/v0.5.9
# Monaco Protocol SDK v0.5.9
## Movements API Cleanup & Order Status Update
This release removes the unsupported `source` filter from movements APIs and adds `SETTLED_ON_CHAIN` as a terminal order status.
***
## Breaking Changes
### `source` Removed from Movements APIs
The `source` parameter has been removed from `getPaginatedUserMovements()` and `useUserMovements()`. It was never supported by the API and would cause a 400 error if passed.
**Affected packages:** `@0xmonaco/types`, `@0xmonaco/core`, `@0xmonaco/react`, `@0xmonaco/mcp-server`
**Migration:**
```typescript theme={null}
// Before — would return a 400 error
await sdk.profile.getPaginatedUserMovements({ source: "hot_storage" });
// After — remove source, other filters still work
await sdk.profile.getPaginatedUserMovements({ page: 1, limit: 20 });
await sdk.profile.getPaginatedUserMovements({ entry_type: "CREDIT" });
await sdk.profile.getPaginatedUserMovements({ transaction_type: "TRADE" });
```
***
## New Features
### `SETTLED_ON_CHAIN` Order Status
`SETTLED_ON_CHAIN` is now a supported `OrderStatus` value. It represents the terminal state where a filled order has been fully settled on-chain.
**Full order lifecycle:**
```
SUBMITTED → PARTIALLY_FILLED → FILLED → SETTLED → SETTLED_ON_CHAIN
```
**Affected packages:** `@0xmonaco/types`, `@0xmonaco/mcp-server`
**Usage:**
```typescript theme={null}
const orders = await sdk.trading.getPaginatedOrders({ status: "SETTLED_ON_CHAIN" });
```
***
## Package Versions
| Package | Version |
| - | - |
| `@0xmonaco/core` | 0.5.9 |
| `@0xmonaco/react` | 0.5.9 |
| `@0xmonaco/types` | 0.5.9 |
| `@0xmonaco/mcp-server` | 0.5.9 |
| `@0xmonaco/contracts` | 0.5.9 |
```bash theme={null}
npm install @0xmonaco/core@0.5.9 @0xmonaco/react@0.5.9
```
All examples in these docs target the **staging** network (`https://staging.apimonaco.xyz`).
# V0.6.0
Source: https://docs.0xmonaco.com/releases/v0.6.0
# Monaco Protocol SDK v0.6.0
## Portfolio Analytics API
This release adds portfolio analytics to the TypeScript SDK, giving developers access to aggregate trading statistics and time-series chart data for the authenticated user.
***
## New Features
### Portfolio Statistics
Retrieve aggregate performance metrics for the authenticated user over a configurable time period.
**New Method:** `sdk.profile.getPortfolioStats(period)`
**Periods:** `"24h"` | `"7d"` | `"30d"` | `"all"`
```typescript theme={null}
import { MonacoSDK } from "@0xmonaco/core";
const sdk = new MonacoSDK({ walletClient, network: "staging", seiRpcUrl: "https://evm-rpc-testnet.sei-apis.com", wsUrl: "wss://staging.apimonaco.xyz/ws" });
// Authenticate first
await sdk.login('your-client-id');
// Get portfolio stats for the last 30 days
const stats = await sdk.profile.getPortfolioStats("30d");
console.log(`PnL: ${stats.pnl}`);
console.log(`Volume: ${stats.volume}`);
console.log(`Total trades: ${stats.total_trades}`);
console.log(`Total orders: ${stats.total_orders}`);
console.log(`Fees paid: ${stats.fees_paid}`);
console.log(`Total equity: ${stats.total_equity}`);
console.log(`Win/loss ratio: ${stats.win_loss_ratio}`);
console.log(`Max drawdown: ${stats.max_drawdown}`);
```
**Response — `PortfolioStats`:**
```typescript theme={null}
interface PortfolioStats {
pnl: string; // Realized profit and loss (quote asset, normalized)
volume: string; // Total trading volume (quote asset, normalized)
max_drawdown: string; // Peak-to-trough drawdown on running PnL
total_orders: number; // Total orders placed
total_trades: number; // Total trades executed
fees_paid: string; // Total fees paid (quote asset, normalized)
win_loss_ratio: string; // Ratio of profitable trades to total trades
total_equity: string; // Sum of all spot balances (quote asset, normalized)
}
```
***
### Portfolio Chart
Retrieve a time-bucketed chart series for volume or PnL over a given period.
**New Method:** `sdk.profile.getPortfolioChart(period, metric)`
**Periods:** `"24h"` | `"7d"` | `"30d"` | `"all"`
**Metrics:** `"volume"` | `"pnl"`
```typescript theme={null}
// Get PnL chart for the last 7 days
const chart = await sdk.profile.getPortfolioChart("7d", "pnl");
console.log(`Metric: ${chart.metric}`);
console.log(`Period: ${chart.period}`);
chart.data.forEach(point => {
console.log(`${point.timestamp}: ${point.value}`);
});
// Get volume chart for all time
const volumeChart = await sdk.profile.getPortfolioChart("all", "volume");
```
**Response — `PortfolioChartResponse`:**
```typescript theme={null}
interface PortfolioChartResponse {
metric: PortfolioMetric; // "volume" | "pnl"
period: PortfolioPeriod; // "24h" | "7d" | "30d" | "all"
data: PortfolioChartPoint[]; // Time-bucketed data points
}
interface PortfolioChartPoint {
timestamp: string; // ISO 8601 bucket start time
value: string; // Metric value for the bucket (normalized)
}
```
***
### User Trade History
Retrieve paginated trade history for the authenticated user, with optional filtering by trading pair.
**New Method:** `sdk.profile.getUserTrades(params?)`
```typescript theme={null}
// Get first page of trade history
const trades = await sdk.profile.getUserTrades({ page: 1, limit: 20 });
trades.trades.forEach(trade => {
console.log(`${trade.side} ${trade.quantity} @ ${trade.price} — fee: ${trade.fee}`);
console.log(`Executed: ${trade.executed_at}`);
});
// Filter by trading pair
const btcTrades = await sdk.profile.getUserTrades({
trading_pair_id: '123e4567-e89b-12d3-a456-426614174000',
});
console.log(`Total trades: ${trades.total_count}, Pages: ${trades.total_pages}`);
```
**Response — `GetUserTradesResponse`:**
```typescript theme={null}
interface GetUserTradesResponse {
trades: UserTrade[];
page: number;
limit: number;
total_count: number;
total_pages: number;
}
interface UserTrade {
trade_id: string; // Trade unique identifier
trading_pair_id: string; // Trading pair UUID
price: string; // Execution price (normalized)
quantity: string; // Executed quantity (normalized)
quote_volume: string; // price × quantity (normalized)
side: "BUY" | "SELL"; // From the user's perspective
fee: string; // Fee paid for this trade (normalized)
executed_at: string; // Trade execution timestamp (ISO 8601)
}
```
***
### Fee Rates on User Profile
`getProfile()` now returns your account's fee rates directly.
```typescript theme={null}
const profile = await sdk.profile.getProfile();
console.log(`Taker fee: ${profile.taker_fee_bps} bps (${profile.taker_fee_bps / 100}%)`);
console.log(`Maker fee: ${profile.maker_fee_bps} bps (${profile.maker_fee_bps / 100}%)`);
```
**New fields on `UserProfile`:**
* `taker_fee_bps` (`number`): Taker fee rate in basis points
* `maker_fee_bps` (`number`): Maker fee rate in basis points
***
## New Types
The following types are now exported from `@0xmonaco/types`:
| Type | Description |
| - | - |
| `PortfolioPeriod` | `"24h" \| "7d" \| "30d" \| "all"` |
| `PortfolioMetric` | `"volume" \| "pnl"` |
| `PortfolioStats` | Aggregate stats response |
| `PortfolioChartPoint` | Single time-series data point |
| `PortfolioChartResponse` | Chart series response |
| `UserTrade` | Single trade execution record |
| `GetUserTradesParams` | Query params for trade history |
| `GetUserTradesResponse` | Paginated trade history response |
***
## Technical Updates
* Stopped publishing declaration and JavaScript source maps that referenced unpublished `src/` files across all packages. No API changes.
***
## Upgrade Instructions
```bash theme={null}
# NPM
npm install @0xmonaco/core@0.6.0 @0xmonaco/types@0.6.0 @0xmonaco/react@0.6.0
# Bun
bun add @0xmonaco/core@0.6.0 @0xmonaco/types@0.6.0 @0xmonaco/react@0.6.0
```
No breaking changes. No migration steps required — this is a purely additive release.
***
*For complete documentation, see the [Profile API guide](/sdk/typescript/profile).*
# V0.6.1
Source: https://docs.0xmonaco.com/releases/v0.6.1
# Monaco Protocol SDK v0.6.1
## Simulate Fees Improvements
This release improves the `/api/v1/fees/simulate` endpoint and SDK to support the 100% balance slider on the frontend and accurate fee estimation for market orders.
***
## New Features
### max\_quantity — 100% Balance Slider Support
`simulateFees()` now returns `max_quantity`: the maximum quantity the authenticated user can buy at the given price, already accounting for fees. Use this directly as the value for a 100% balance slider instead of computing `available_balance / price` (which ignores fees and causes insufficient balance errors).
```typescript theme={null}
const fees = await sdk.fees.simulateFees({
trading_pair_id: "123e4567-e89b-12d3-a456-426614174000",
side: "BUY",
price: "68000.00",
quantity: "0.001",
});
// Use max_quantity for the 100% slider value
console.log(`Max quantity you can buy: ${fees.max_quantity}`);
console.log(`Max quantity (raw units): ${fees.max_quantity_raw}`);
```
**New response fields:**
| Field | Type | Description |
| - | - | - |
| `max_quantity` | `string \| null` | Maximum affordable quantity at the given price, after fees |
| `max_quantity_raw` | `string \| null` | Same value in raw (smallest unit) format |
***
### Market Order Slippage Buffer
Pass `order_type: "MARKET"` to get a slippage-adjusted `buy_order_lock_amount`. This reflects the worst-case execution cost for a market order instead of the exact-price calculation, preventing false "affordable" signals when the actual fill price differs due to slippage.
```typescript theme={null}
// LIMIT — exact price fee calculation
const limitFees = await sdk.fees.simulateFees({
trading_pair_id: "123e4567-e89b-12d3-a456-426614174000",
side: "BUY",
price: "68000.00",
quantity: "0.001",
order_type: "LIMIT",
});
// MARKET — includes 5% slippage buffer in buy_order_lock_amount
const marketFees = await sdk.fees.simulateFees({
trading_pair_id: "123e4567-e89b-12d3-a456-426614174000",
side: "BUY",
price: "68000.00",
quantity: "0.001",
order_type: "MARKET",
});
console.log(`LIMIT lock: ${limitFees.buy_order_lock_amount}`);
console.log(`MARKET lock: ${marketFees.buy_order_lock_amount}`); // higher — includes slippage
console.log(`Slippage tolerance: ${marketFees.slippage_tolerance_bps} bps`);
// Custom slippage tolerance (0–1000 bps)
const customSlippage = await sdk.fees.simulateFees({
trading_pair_id: "123e4567-e89b-12d3-a456-426614174000",
side: "BUY",
price: "68000.00",
quantity: "0.001",
order_type: "MARKET",
slippage_tolerance_bps: 300, // 3%
});
```
**New request params:**
| Param | Type | Description |
| - | - | - |
| `order_type` | `"LIMIT" \| "MARKET"` (optional) | Defaults to `LIMIT` behaviour if omitted |
| `slippage_tolerance_bps` | `number` (optional, 0–1000) | Only valid with `order_type: "MARKET"`. Default: `500` (5%) |
**New response field:**
| Field | Type | Description |
| - | - | - |
| `slippage_tolerance_bps` | `number \| null` | Slippage tolerance used in the calculation, echoed back. `null` for LIMIT orders |
***
## Updated Types
**`SimulateFeeParams`** (`@0xmonaco/types`):
* Added `order_type?: "LIMIT" | "MARKET"`
* Added `slippage_tolerance_bps?: number` (0–1000, MARKET only)
**`SimulateFeeResponse`** (`@0xmonaco/types`):
* Added `max_quantity: string | null`
* Added `max_quantity_raw: string | null`
* Added `slippage_tolerance_bps: number | null`
***
## Upgrade Instructions
```bash theme={null}
# NPM
npm install @0xmonaco/core@0.6.1 @0xmonaco/types@0.6.1 @0xmonaco/react@0.6.1
# Bun
bun add @0xmonaco/core@0.6.1 @0xmonaco/types@0.6.1 @0xmonaco/react@0.6.1
```
No breaking changes. All new params and response fields are optional or nullable.
***
*For complete documentation, see the [Fees API reference](/api-reference/fees/simulate-fees-for-an-order-before-placing-it).*
# V0.6.2
Source: https://docs.0xmonaco.com/releases/v0.6.2
# Monaco Protocol SDK v0.6.2
## Bug Fixes
### `revokeToken()` no longer accepts a parameter
`sdk.auth.revokeToken()` previously accepted a `refreshToken` argument, but the server identifies the session to revoke from the access token in the `Authorization` header — the body was always ignored. Passing the argument caused a silent `400 Bad Request: Revoke request body must be empty` error, meaning token revocation (and therefore `sdk.logout()`) was silently failing.
If you call `sdk.auth.revokeToken()` directly, remove the argument. `sdk.logout()` requires no changes.
```diff theme={null}
- await sdk.auth.revokeToken(authState.refreshToken);
+ await sdk.auth.revokeToken();
```
**Affected packages:** `@0xmonaco/core`, `@0xmonaco/react`, `@0xmonaco/types`
***
### MCP server: network config passed explicitly to `registerTools`
Network configuration is now passed explicitly to `registerTools` instead of being read from `process.env` at registration time. This fixes flaky CI tests caused by parallel test files racing on shared `process.env` mutations. No changes required for MCP server users.
***
## Upgrade Instructions
```bash theme={null}
# NPM
npm install @0xmonaco/core@0.6.2 @0xmonaco/types@0.6.2 @0xmonaco/react@0.6.2
# Bun
bun add @0xmonaco/core@0.6.2 @0xmonaco/types@0.6.2 @0xmonaco/react@0.6.2
```
# V0.6.3
Source: https://docs.0xmonaco.com/releases/v0.6.3
# Monaco Protocol SDK v0.6.3
## Bug Fixes
### MCP server: test isolation improvements
Added `resetSDK` to clear the singleton SDK instance between tests, ensuring each test starts with a clean state. No changes required for MCP server users.
**Affected packages:** `@0xmonaco/contracts`, `@0xmonaco/core`, `@0xmonaco/mcp-server`, `@0xmonaco/react`, `@0xmonaco/types`
***
## Upgrade Instructions
```bash theme={null}
# NPM
npm install @0xmonaco/core@0.6.3 @0xmonaco/types@0.6.3 @0xmonaco/react@0.6.3
# Bun
bun add @0xmonaco/core@0.6.3 @0xmonaco/types@0.6.3 @0xmonaco/react@0.6.3
```
# V0.7.0
Source: https://docs.0xmonaco.com/releases/v0.7.0
# Monaco Protocol SDK v0.7.0
## Perpetuals on Monaco
Monaco's first-class perp surface ships with v0.7.0. The SDK gains two new top-level modules — `sdk.marginAccounts` and `sdk.positions` — plus perp helpers on `sdk.market`, perp options on `sdk.trading`, a new `usePositions` React hook, and a `conditional_order_update` WebSocket event for TP/SL lifecycle.
Spot trading is unchanged. All existing spot endpoints, hooks, and patterns continue to work as in v0.6.x.
***
## New Features
### Margin Accounts
A margin account is the reusable parent collateral account that backs isolated perp buckets. Each parent holds a single collateral asset (USDC by default) and exposes equity, margin requirements, free collateral, and unrealized PnL.
**New module:** `sdk.marginAccounts`
```typescript theme={null}
import { MonacoSDK } from "@0xmonaco/core";
// Ensure the parent margin account exists (USDC by default)
const account = await sdk.marginAccounts.ensureParentMarginAccount({
label: "perp-main",
collateralAsset: "USDC",
});
// Read live state
const summary = await sdk.marginAccounts.getMarginAccountSummary(
account.margin_account_id,
);
// summary.equity, summary.free_collateral,
// summary.maintenance_margin_required, summary.unrealized_pnl, …
// Move collateral in / out
await sdk.marginAccounts.transferCollateralToMarginAccount(
account.margin_account_id,
{ asset: "USDC", amount: "1000" },
);
await sdk.marginAccounts.transferCollateralFromMarginAccount(
account.margin_account_id,
{ asset: "USDC", amount: "250" },
);
// What's available in the wallet vs. transferable to margin
const collateral = await sdk.marginAccounts.getAvailableCollateral({
asset: "USDC",
});
// Pre-flight: would this order be accepted?
const risk = await sdk.marginAccounts.simulateOrderRisk(
account.margin_account_id,
{
tradingPairId,
side: "BUY",
positionSide: "LONG",
orderType: "LIMIT",
price: "2000",
quantity: "1.5",
leverage: "10",
reduceOnly: false,
},
);
// risk.accepted, risk.estimated_liquidation_price,
// risk.free_collateral_after, risk.estimated_fee, …
// Collateral transfers recorded against the margin account
const movements = await sdk.marginAccounts.getMarginAccountMovements(
account.margin_account_id,
{ page: 1, page_size: 50 },
);
```
**Methods:**
| Method | Returns |
| - | - |
| `listMarginAccounts(params?)` | `ListMarginAccountsResponse` |
| `ensureParentMarginAccount(request?)` | `EnsureParentMarginAccountResponse` |
| `getMarginAccountSummary(id)` | `MarginAccountSummary` |
| `getAvailableCollateral(params?)` | `GetAvailableCollateralResponse` |
| `transferCollateralToMarginAccount(id, request)` | `TransferCollateralResponse` |
| `transferCollateralFromMarginAccount(id, request)` | `TransferCollateralResponse` |
| `getMarginAccountMovements(id, params?)` | `GetMarginAccountMovementsResponse` |
| `simulateOrderRisk(id, request)` | `SimulateOrderRiskResponse` |
**`MarginAccountSummary`:**
```typescript theme={null}
interface MarginAccountSummary {
margin_account_id: string;
label?: string;
account_state: string;
equity: string;
initial_margin_required: string;
maintenance_margin_required: string;
free_collateral: string; // available to open new positions
withdrawable_collateral: string; // can be transferred out
total_position_notional: string;
unrealized_pnl: string;
realized_pnl: string;
updated_at: string;
}
```
***
### Positions
A position is the open exposure on a perp pair backed by a margin account. The `sdk.positions` module covers the full lifecycle — open (via order placement), modify isolated margin, attach TP/SL, close (full or partial), inspect risk.
**New module:** `sdk.positions`
```typescript theme={null}
// List open positions in a margin account
const open = await sdk.positions.listPositions({
margin_account_id,
status: "OPEN",
});
// Inspect a single position
const position = await sdk.positions.getPosition(positionId);
// position.side ("LONG"|"SHORT"), position.size, position.entry_price,
// position.mark_price, position.liquidation_price, position.unrealized_pnl,
// position.leverage, position.isolated_margin, position.status, …
// Live risk snapshot — useful for a continuously-updating UI
const risk = await sdk.positions.getPositionRisk(positionId);
// risk.mark_price, risk.liquidation_price, risk.margin_ratio, …
// Close: full market close, partial market close, limit close, or IOC
await sdk.positions.closePosition(positionId, { closeType: "MARKET" });
await sdk.positions.closePosition(positionId, {
closeType: "LIMIT",
limitPrice: "2050.00",
quantity: "0.5",
});
// Adjust isolated margin on an open position
await sdk.positions.addPositionMargin(positionId, {
amount: "200",
asset: "USDC",
});
await sdk.positions.reducePositionMargin(positionId, { amount: "100" });
// Attach TP and/or SL — both legs are optional
await sdk.positions.attachPositionTpSl(positionId, {
takeProfit: { triggerPrice: "2200", orderType: "MARKET" },
stopLoss: {
triggerPrice: "1900",
orderType: "LIMIT",
limitPrice: "1895",
},
});
// Position lifecycle events (open, fills, funding, liquidations, close)
const history = await sdk.positions.listPositionHistory({
position_id: positionId,
});
```
**Methods:**
| Method | Returns |
| - | - |
| `listPositions(params?)` | `ListPositionsResponse` |
| `getPosition(id)` | `Position` |
| `closePosition(id, request)` | `ClosePositionResponse` |
| `getPositionRisk(id)` | `PositionRisk` |
| `addPositionMargin(id, request)` | `PositionMarginResponse` |
| `reducePositionMargin(id, request)` | `PositionMarginResponse` |
| `attachPositionTpSl(id, request)` | `AttachPositionTpSlResponse` |
| `listPositionHistory(params?)` | `ListPositionHistoryResponse` |
**`Position`:**
```typescript theme={null}
interface Position {
position_id: string;
margin_account_id: string;
trading_pair_id: string;
side: "LONG" | "SHORT" | "NONE";
size: string;
entry_price: string;
mark_price: string;
index_price?: string;
unrealized_pnl: string;
realized_pnl: string;
isolated_margin: string;
leverage?: string;
maintenance_margin_required: string;
initial_margin_required?: string;
liquidation_price: string;
status: "OPEN" | "CLOSED" | "LIQUIDATING";
updated_at: string;
}
```
`closePosition` accepts `{ closeType: "MARKET" | "LIMIT" | "IOC" }` plus optional `limitPrice`, `quantity` (for partial close), and `slippageToleranceBps`.
***
### Perp options on `placeLimitOrder` / `placeMarketOrder`
Existing order placement methods now accept perp options. Spot order placement continues to work unchanged — the new options are additive.
```typescript theme={null}
// Open a 10x long via limit order
await sdk.trading.placeLimitOrder(tradingPairId, "BUY", "1.5", "2000", {
marginAccountId,
positionSide: "LONG",
leverage: "10",
});
// Reduce-only market close of a short
await sdk.trading.placeMarketOrder(tradingPairId, "BUY", "0.5", {
marginAccountId,
positionSide: "SHORT",
reduceOnly: true,
});
```
**New options on both methods:**
| Option | Type | Notes |
| - | - | - |
| `marginAccountId` | `string` | Required for perp orders |
| `positionSide` | `"LONG" \| "SHORT" \| "NONE"` | Required for perp orders |
| `leverage` | `string` | Decimal string, e.g. `"10"` |
| `reduceOnly` | `boolean` | Order can only reduce existing exposure |
***
### Perp market data on `sdk.market`
Real-time perp metrics are available through the existing `sdk.market` module.
```typescript theme={null}
// Static perp config: tick/lot size, leverage tiers, margin ratios, funding interval
const config = await sdk.market.getPerpMarketConfig(tradingPairId);
// Aggregate market snapshot
const summary = await sdk.market.getPerpMarketSummary(tradingPairId);
// summary.mark_price, summary.index_price, summary.open_interest,
// summary.current_funding_rate, summary.next_funding_time, …
// Individual metrics
const mark = await sdk.market.getMarkPrice(tradingPairId);
const index = await sdk.market.getIndexPrice(tradingPairId);
const funding = await sdk.market.getFundingState(tradingPairId);
const oi = await sdk.market.getOpenInterest(tradingPairId);
// Historical funding payments
const records = await sdk.market.listFundingHistory(tradingPairId, {
page: 1,
page_size: 100,
});
```
**New methods:** `getPerpMarketConfig`, `getPerpMarketSummary`, `getMarkPrice`, `getIndexPrice`, `getFundingState`, `getOpenInterest`, `listFundingHistory`.
***
### Standalone conditional orders
In addition to attaching TP/SL through `attachPositionTpSl`, conditional orders can be created standalone via `sdk.trading`:
```typescript theme={null}
const conditional = await sdk.trading.createConditionalOrder({
tradingPairId,
marginAccountId,
conditionType: "STOP_LOSS",
triggerPrice: "1900",
triggerSource: "MARK_PRICE",
side: "SELL",
positionSide: "LONG",
orderType: "MARKET",
reduceOnly: true,
});
await sdk.trading.cancelConditionalOrder(conditional.conditional_order_id);
const open = await sdk.trading.listConditionalOrders({
margin_account_id,
state: "ACTIVE",
});
```
States: `"ACTIVE" | "TRIGGERING" | "TRIGGERED" | "CANCELLED" | "EXPIRED" | "FAILED"`.
***
### `conditional_order_update` WebSocket event
TP/SL lifecycle updates stream over the existing authenticated websocket.
```typescript theme={null}
const unsubscribe = sdk.ws.conditionalOrders((evt) => {
// evt.eventType === "conditional_order_update"
// evt.data.conditionalOrderId, evt.data.state, evt.data.reason,
// evt.data.triggeredOrderId, evt.data.triggerPrice, …
}, tradingPairId /* optional — omit for all pairs */);
// Later
unsubscribe();
```
`reason` is one of `"created" | "cancelled" | "triggered" | "failed" | "oco_cancelled"`.
***
### React: `usePositions` hook
```tsx theme={null}
import { usePositions } from "@0xmonaco/react";
function PositionsPanel({ marginAccountId }: { marginAccountId: string }) {
const {
listPositions,
getPosition,
getPositionRisk,
closePosition,
addPositionMargin,
reducePositionMargin,
attachPositionTpSl,
listPositionHistory,
} = usePositions();
// …
}
```
`useMarket` is extended with `getPerpMarketConfig`, `getPerpMarketSummary`, `getMarkPrice`, `getIndexPrice`, `getFundingState`, `listFundingHistory`, `getOpenInterest`.
`useTrade`'s `placeLimitOrder` and `placeMarketOrder` accept the same perp options shown above. `useTrade` also exposes `createConditionalOrder`, `cancelConditionalOrder`, `listConditionalOrders`.
***
## Breaking Changes
### Pagination standardized to `page` / `page_size`
`limit`, `offset`, and `skip` query parameters have been removed across all paginated endpoints in favor of `page` / `page_size`. `total_count` was renamed to `total`. `total_pages` was added to `GetUserBalancesResponse`.
```typescript theme={null}
// Before
const trades = await sdk.profile.getUserTrades({ limit: 20, offset: 0 });
const balances = await sdk.profile.getUserBalances({ limit: 50, offset: 0 });
// After
const trades = await sdk.profile.getUserTrades({ page: 1, page_size: 20 });
const balances = await sdk.profile.getUserBalances({ page: 1, page_size: 50 });
```
`@0xmonaco/mcp-server`: the `get-trades` MCP tool parameters changed from `skip`/`limit` to `page`/`page_size`.
***
### `Order.trading_pair` → `Order.trading_pair_id`
Trading-pair identifier fields have been standardized across the API.
```typescript theme={null}
// Before
order.trading_pair // pair UUID
profileOrder.trading_pair // pair string like "ETH-USD"
// After
order.trading_pair_id // pair UUID
profileOrder.symbol // pair string like "ETH-USD"
```
Same change applies to `GetPaginatedOrdersParams.trading_pair` → `.trading_pair_id`. WebSocket events: `tradingPair` → `symbol` on order events.
***
### Flattened response shapes (trades & market)
The `data` wrapper has been removed from trades and market responses.
```typescript theme={null}
// Before
const response = await sdk.market.getPaginatedTradingPairs();
const pairs = response.data.data; // TradingPair[]
// After
const response = await sdk.market.getPaginatedTradingPairs();
const pairs = response.trading_pairs; // TradingPair[]
```
The `PaginatedTradingPairs` interface has been removed; pagination fields (`page`, `page_size`, `total`, `total_pages`) now live directly on the response.
***
### Removed `success` field from response types
HTTP status codes already convey success/failure. The redundant `success` boolean has been dropped from:
* `BatchCancelOrdersResponse`, `BatchCancelResult`
* `BatchCreateOrdersResponse`, `BatchCreateResult`
* `BatchReplaceOrdersResponse`, `BatchReplaceResult`
* `GetTradingPairsResponse`, `GetTradingPairResponse`
* `GetCandlestickResponse`
For batch operations, check `total_failed === 0` or inspect individual result `error` fields.
***
### Close-position requests
`position_id` is now only accepted in the URL path, not the request body.
```typescript theme={null}
// Before — would also accept position_id in body
await sdk.positions.closePosition(positionId, { closeType: "MARKET", position_id: positionId });
// After
await sdk.positions.closePosition(positionId, { closeType: "MARKET" });
```
***
## New Types
The following types are now exported from `@0xmonaco/types`:
**Margin Accounts** — `MarginAccountSummary`, `ListMarginAccountsParams`, `ListMarginAccountsResponse`, `EnsureParentMarginAccountRequest`, `EnsureParentMarginAccountResponse`, `GetAvailableCollateralParams`, `GetAvailableCollateralResponse`, `TransferCollateralRequest`, `TransferCollateralResponse`, `GetMarginAccountMovementsParams`, `GetMarginAccountMovementsResponse`, `MarginAccountMovement`, `SimulateOrderRiskRequest`, `SimulateOrderRiskResponse`, `MarginAccountsAPI`.
**Positions** — `Position`, `PositionStatus`, `ClosePositionType`, `ListPositionsParams`, `ListPositionsResponse`, `GetPositionResponse`, `ClosePositionRequest`, `ClosePositionResponse`, `PositionRisk`, `AddPositionMarginRequest`, `ReducePositionMarginRequest`, `PositionMarginResponse`, `TpSlLeg`, `AttachPositionTpSlRequest`, `AttachPositionTpSlResponse`, `ListPositionHistoryParams`, `ListPositionHistoryResponse`, `PositionHistoryEvent`, `PositionsAPI`.
**Market (perp)** — `PerpMarketConfig`, `PerpMarketSummary`, `MarkPrice`, `IndexPrice`, `IndexComponent`, `FundingState`, `FundingRecord`, `ListFundingHistoryParams`, `ListFundingHistoryResponse`, `OpenInterest`, `RiskTier`.
**Conditional orders** — `ConditionalOrderConditionType`, `ConditionalOrderTriggerSource`, `ConditionalOrderState`, `CreateConditionalOrderParams`, `CreateConditionalOrderResponse`, `CancelConditionalOrderResponse`, `ListConditionalOrdersParams`, `ListConditionalOrdersResponse`, `ConditionalOrderEvent`, `ConditionalOrderEventData`, `ConditionalOrderEventReason`.
**Trading** — `PositionSide`, `TradingMode`. `Order` gains `margin_account_id`, `position_side`, `leverage`, `reduce_only`, `position_id` fields.
***
## Upgrade Instructions
```bash theme={null}
# NPM
npm install @0xmonaco/core@0.7.0 @0xmonaco/types@0.7.0 @0xmonaco/react@0.7.0
# Bun
bun add @0xmonaco/core@0.7.0 @0xmonaco/types@0.7.0 @0xmonaco/react@0.7.0
```
**Migration checklist:**
1. Replace `limit`/`offset`/`skip` with `page`/`page_size` in any paginated calls.
2. Update field reads: `Order.trading_pair` → `.trading_pair_id`, `ProfileOrder.trading_pair` → `.symbol`.
3. Update WebSocket order-event handlers reading `tradingPair` → `symbol`.
4. Unwrap `data` from market and trades responses (`response.data.data` → `response.trading_pairs`).
5. Drop reads of `success` on batch / trading-pair / candlestick responses; check `total_failed` or HTTP status instead.
***
*For complete documentation, see [Perpetuals (perps)](/trading-mechanics/perps), [Perps Accounts](/developers/perps-accounts), [Positions](/developers/positions), [Margin Accounts SDK reference](/sdk/typescript/margin-accounts), and [Positions SDK reference](/sdk/typescript/positions).*
# V0.7.2
Source: https://docs.0xmonaco.com/releases/v0.7.2
# Monaco Protocol SDK v0.7.2
## Vault Deposits
Monaco's vault contract has been upgraded. Deposits now embed the application's
`client_id` directly in the on-chain call. The public `sdk.vault.deposit()`
signature is unchanged — no code changes required for existing callers.
### What changed
* `@0xmonaco/contracts`: Vault ABI updated. ERC20 deposits now call
`depositERC20(destination, clientId, token, amount)`; native SEI deposits
use `depositNative(destination)`.
* `@0xmonaco/core`: `sdk.vault.deposit()` reads `clientId` from
`applications.getApplicationConfig()` and calls the new entrypoints.
Public method signature unchanged.
* `@0xmonaco/types`: `ApplicationConfigResponse` now includes `clientId`.
### No migration required
`sdk.vault.deposit(assetId, amount)` behaves identically for callers.
***
## Margin Transfers — Balance Coherence
Collateral movement now keeps wallet balances, WebSocket balance events, and
UI state aligned after margin transfers.
### New balance event reasons
`UserBalanceEvent.data.reason` has two new values:
| Reason | Description |
| - | - |
| `margin_transfer_in` | Collateral moved from wallet into a margin account |
| `margin_transfer_out` | Collateral moved from a margin account back to the wallet |
### `TransferCollateralResponse` — new fields
Both `transferCollateralToMarginAccount()` and `transferCollateralFromMarginAccount()`
now return:
| Field | Type | Description |
| - | - | - |
| `new_total_collateral_value` | string | Total collateral value after the transfer |
| `new_withdrawable_collateral` | string | Amount withdrawable from the account after the transfer |
Use these to update the UI immediately without a separate refresh call.
### `useUserBalances` behavior (React)
Spot-only balance events (order fills, locks) now preserve the existing
margin-collateral component of `total_balance`. A `margin_transfer_in` or
`margin_transfer_out` event replaces the full total. This prevents the wallet
total from briefly dropping after a spot event when collateral is also locked
in a margin account.
***
## Upgrade
```bash theme={null}
npm install @0xmonaco/core@0.7.2 @0xmonaco/types@0.7.2 @0xmonaco/react@0.7.2
# or bun
bun add @0xmonaco/core@0.7.2 @0xmonaco/types@0.7.2 @0xmonaco/react@0.7.2
```
No breaking changes. No migration steps required.
# V0.7.8
Source: https://docs.0xmonaco.com/releases/v0.7.8
# Monaco Protocol SDK v0.7.8
## Withdrawal Flow
The withdrawal flow has been updated to use a pre-signed calldata path through
the backend. `sdk.vault.withdraw()` now posts to `/api/v1/withdrawals` to receive
gateway-signed calldata and a `withdrawalIndex`, then submits the transaction
on-chain via the wallet client.
### Return type change
`withdraw()` now returns `WithdrawResult` instead of `TransactionResult`:
```typescript theme={null}
interface WithdrawResult extends TransactionResult {
withdrawalIndex: number;
}
```
Store `withdrawalIndex` — you need it to retry a failed submission.
### New method: `retryWithdrawal`
```typescript theme={null}
const { withdrawalIndex } = await sdk.vault.withdraw(assetId, amount);
// If the on-chain submission fails or is dropped, retry without
// allocating a new withdrawal slot:
await sdk.vault.retryWithdrawal(withdrawalIndex);
```
`retryWithdrawal` fetches the same gateway-signed calldata originally issued for
that index and resubmits it. Safe to call multiple times — the matching engine
is idempotent against duplicate submissions of a settled withdrawal.
### Migration
```diff theme={null}
- const result = await sdk.vault.withdraw(assetId, amount);
- console.log(result.hash);
+ const result = await sdk.vault.withdraw(assetId, amount);
+ console.log(result.hash);
+ console.log(result.withdrawalIndex); // new — store for retry
```
If you only use `hash` or `status` from the return value, no code change is
required. The new `withdrawalIndex` field is additive.
***
## Position Endpoints — `position_id` Body Rejection
The API gateway now rejects unknown fields in JSON request bodies. As a result,
`position_id` is no longer accepted in the body of:
* `POST /api/v1/positions/{id}/close`
* `POST /api/v1/positions/{id}/add-margin`
* `POST /api/v1/positions/{id}/reduce-margin`
The position ID must be in the URL path only. **SDK callers are unaffected** —
the TypeScript SDK has always derived the path from the `positionId` argument
and never included it in the body.
**If you call these endpoints directly over HTTP**, remove `position_id` from the
request body:
```diff theme={null}
// close
- POST /api/v1/positions/{id}/close
- { "position_id": "...", "close_type": "MARKET" }
+ POST /api/v1/positions/{id}/close
+ { "close_type": "MARKET" }
// add margin
- POST /api/v1/positions/{id}/add-margin
- { "position_id": "...", "amount": "200", "asset": "USDC" }
+ POST /api/v1/positions/{id}/add-margin
+ { "amount": "200", "asset": "USDC" }
// reduce margin
- POST /api/v1/positions/{id}/reduce-margin
- { "position_id": "...", "amount": "100" }
+ POST /api/v1/positions/{id}/reduce-margin
+ { "amount": "100" }
```
***
## Upgrade
```bash theme={null}
npm install @0xmonaco/core@0.7.8 @0xmonaco/types@0.7.8 @0xmonaco/react@0.7.8
# or bun
bun add @0xmonaco/core@0.7.8 @0xmonaco/types@0.7.8 @0xmonaco/react@0.7.8
```
# V0.7.9
Source: https://docs.0xmonaco.com/releases/v0.7.9
# Monaco Protocol SDK v0.7.9
## Parent TP/SL at Order Placement
You can now attach take-profit and stop-loss brackets directly when placing a
limit or market order. Previously, TP/SL required a separate
`createConditionalOrder` call after the order was open. With parent TP/SL, the
legs are submitted atomically with the entry order and the matching engine owns
their full lifecycle.
### Attaching brackets on placement
Pass `takeProfit` and/or `stopLoss` in the options of `placeLimitOrder` or
`placeMarketOrder`. Both are margin-only and cannot be combined with
`reduceOnly: true`.
```typescript theme={null}
const order = await sdk.trading.placeLimitOrder(tradingPairId, "BUY", "1.5", "2000", {
tradingMode: "MARGIN",
marginAccountId,
positionSide: "LONG",
leverage: "10",
takeProfit: {
triggerPrice: "2200",
orderType: "MARKET",
},
stopLoss: {
triggerPrice: "1900",
orderType: "LIMIT",
limitPrice: "1895",
timeInForce: "IOC",
},
});
// Conditional order IDs for the attached legs
order.take_profit_order_id;
order.stop_loss_order_id;
```
The same options are available on `placeMarketOrder`.
### Leg shape
```typescript theme={null}
interface ParentTpSlLegParams {
triggerPrice: string;
orderType: "MARKET" | "LIMIT";
limitPrice?: string; // required for LIMIT
timeInForce?: "GTC" | "IOC"; // for LIMIT orders
slippageToleranceBps?: number; // for MARKET orders
expiresAt?: string; // ISO 8601 — auto-cancel if not triggered
}
```
The leg has no `quantity` field — it inherits the parent order's quantity.
### Lifecycle
If the parent fills immediately, the attached TP/SL legs move directly to
`ACTIVE`. If the parent rests on the book, the legs start as `PENDING_PARENT`
and activate once the parent fill creates or extends a position.
| State | Meaning |
| - | - |
| `PENDING_PARENT` | Parent order has not filled yet |
| `ACTIVE` | Waiting for trigger price to be crossed |
| `TRIGGERING` | Trigger fired, underlying order being submitted |
| `TRIGGERED` | Underlying order submitted |
| `CANCELLED` | Cancelled by user, OCO sibling, or parent cancellation |
**OCO behaviour:** when one leg triggers, the matching engine automatically
cancels the sibling. Cancelling the unfilled parent order also cancels any
`PENDING_PARENT` children.
### New `ConditionalOrder` fields
`listConditionalOrders` responses now include:
| Field | Description |
| - | - |
| `parent_order_id` | UUID of the parent order this leg is attached to |
| `association_type` | `"PARENT_ORDER"` for parent-attached legs, `"POSITION"` for position-attached |
| `linked_group_id` | Shared UUID across the TP and SL of the same parent — useful for rendering OCO pairs |
| `activated_at` | Timestamp when a `PENDING_PARENT` leg became `ACTIVE` |
### WebSocket changes
The `conditional_order_update` event gains matching fields and two new reason
values:
```typescript theme={null}
{
// new fields
parentOrderId?: string;
associationType?: "POSITION" | "PARENT_ORDER";
linkedGroupId?: string;
activatedAt?: string;
state: "PENDING_PARENT" | "ACTIVE" | "TRIGGERING" | "TRIGGERED" | "CANCELLED" | "EXPIRED" | "FAILED";
reason: "created" | "activated" | "cancelled" | "parent_cancelled" | "triggered" | "failed" | "oco_cancelled";
}
```
New `reason` values:
* `activated` — `PENDING_PARENT` leg became `ACTIVE` after the parent filled
* `parent_cancelled` — parent order was cancelled before filling, leg was purged
* `oco_cancelled` — sibling leg triggered first, this leg was cancelled by OCO
### Deprecated: standalone `createConditionalOrder`
`sdk.trading.createConditionalOrder` is deprecated for TP/SL use. New
integrations should use parent TP/SL (this release) or
`sdk.positions.attachPositionTpSl` for existing positions. The method remains
available but will be removed in a future release.
***
## Migration
No breaking changes. All new fields are additive.
If you use `createConditionalOrder` today and want to migrate to parent TP/SL:
```diff theme={null}
- const order = await sdk.trading.placeLimitOrder(tradingPairId, "BUY", "1.5", "2000", {
- tradingMode: "MARGIN",
- marginAccountId,
- positionSide: "LONG",
- leverage: "10",
- });
-
- await sdk.trading.createConditionalOrder({
- tradingPairId,
- marginAccountId,
- conditionType: "TAKE_PROFIT",
- triggerPrice: "2200",
- side: "SELL",
- positionSide: "LONG",
- orderType: "MARKET",
- });
+ const order = await sdk.trading.placeLimitOrder(tradingPairId, "BUY", "1.5", "2000", {
+ tradingMode: "MARGIN",
+ marginAccountId,
+ positionSide: "LONG",
+ leverage: "10",
+ takeProfit: { triggerPrice: "2200", orderType: "MARKET" },
+ });
```
***
## Upgrade
```bash theme={null}
npm install @0xmonaco/core@0.7.9 @0xmonaco/types@0.7.9 @0xmonaco/react@0.7.9
# or bun
bun add @0xmonaco/core@0.7.9 @0xmonaco/types@0.7.9 @0xmonaco/react@0.7.9
```
# V0.8.10
Source: https://docs.0xmonaco.com/releases/v0.8.10
# Monaco Protocol SDK v0.8.10
This release closes the remaining gaps between the SDK surfaces and the live API: several new client surfaces (withdrawals, sub-accounts, faucet, application reporting), a reworked margin-account provisioning model, and full REST↔gRPC parity now that both transports share one set of handlers. It also removes a handful of dead endpoints and methods.
## Breaking changes
### Margin account provisioning is now implicit
The public `POST /api/v1/margin/accounts` provisioning endpoint and the SDK methods that called it (`createMarginAccount` / `ensureParentMarginAccount`) are **removed**. There is one parent margin account per wallet per application scope, and it is created automatically the first time you move collateral into a margin bucket.
Fund (and implicitly provision) a margin account with a collateral transfer-in:
```typescript theme={null}
await sdk.marginAccounts.transferCollateralToAutoMarginAccount({
asset: "USDC",
amount: "100",
tradingPairId: "",
});
```
`listMarginAccounts()` now returns the parent account row plus separate risk-bucket rows, and `getMarginAccountSummary(marginAccountId)` returns the parent summary — pass `{ tradingPairId }` to get a specific bucket summary. Summaries carry `margin_bucket_id` and `margin_mode` (`ISOLATED` / `CROSS`); parent rows leave the bucket fields unset.
### Standalone conditional-order creation removed
`tradingAPI.createConditionalOrder()`, the React `useTrade().createConditionalOrder` method, and `POST /api/v1/orders/conditional` are **removed** (the endpoint had no handler and returned 404). Attach take-profit / stop-loss when placing the order, or use `sdk.positions.attachPositionTpSl(...)` (`POST /api/v1/positions/{position_id}/tp-sl`). Listing and cancelling conditional orders is unchanged.
### `GET /api/v1/trades/user` removed from REST
This endpoint duplicated `GET /api/v1/accounts/trades` and is now **gRPC-only** (`TradesService.ListUserTrades`). REST clients should use `sdk.profile.getUserTrades()` / `GET /api/v1/accounts/trades`.
### Rust REST SDK: `refresh_token` → `refresh_session`
The Rust REST SDK auth method is renamed `refresh_session` (the endpoint, `POST /api/v1/auth/refresh`, is unchanged). This completes the JWT→session-key naming cutover started in v0.8.8.
## Added
### Withdrawals client
A low-level `sdk.withdrawals` surface plus REST `POST /api/v1/withdrawals` and `GET /api/v1/withdrawals/{withdrawal_index}` (and a gRPC `WithdrawalsService`):
* `initiateWithdrawal({ assetId, amount, destination })` (authenticated) debits the balance and returns `{ withdrawal_index, vault_address, calldata }` — the signed `executeSignedWithdrawal` calldata.
* `getWithdrawal(withdrawalIndex)` (public) re-fetches that signed calldata.
These return signed calldata without broadcasting. `sdk.vault.withdraw` remains the high-level flow that also submits the on-chain transaction.
### Delegated-agent owner reverse-lookup
`sdk.delegatedAgents.listDelegatedOwners()` (`GET /api/v1/delegated-agents/owners`) lets an agent discover which owners have an active delegation to it — keyed on the agent's own wallet address. Each entry carries the `owner_user_id` to pass into `createDelegatedSession`, so the owner id no longer has to be supplied out of band. The full delegated-agents surface is now proto-first and appears in the OpenAPI spec and the gRPC SDK.
### New account-management surfaces
* **Sub-accounts** — `sdk.subAccounts`: `list()`, `createLimit()`, `getLimits()`, `updateLimit()`, `deleteLimit()` (limit mutations require the `ManageSubAccounts` permission).
* **Faucet** — `sdk.faucet.mint()` (testnet-only, rate-limited).
* **Funding payments** — `sdk.profile.listFundingPayments(params?)` (`GET /api/v1/accounts/funding-payments`).
* **Application reporting** (backend / `x-server-key` auth) — `applications.listApplicationOrders()`, `listApplicationUsers()`, `listApplicationMovements()`, `listApplicationBalances()`, and `getApplicationStats()`.
### Market
* `sdk.market.getTradingPair(tradingPairId)` — fetch a single pair by UUID.
* `sdk.market.getScreener(params?)` — paginated market screener with 1h/24h/7d quote-volume and price-change windows plus a 7-day daily snapshot.
### Trades
* `sdk.trades.getTradeById(tradeId)` — fetch a single public trade.
### gRPC AccountsService implemented
`GetUserTrades`, `GetPortfolioStats`, and `GetPortfolioChart` are now implemented over gRPC (previously `UNIMPLEMENTED`) and mirror the REST endpoints exactly.
### Generated wire-types
`@0xmonaco/types` now ships a generated `@0xmonaco/types/wire` namespace (raw request/response schemas generated from the canonical OpenAPI spec) plus an `OPERATION_IDS` list. The hand-written ergonomic types assert field coverage against the wire schemas, so a missing field becomes a compile error.
### Rust REST SDK resync
The Rust REST SDK (`monaco-sdk`) was resynced from the canonical OpenAPI spec, adding \~38 previously-absent methods across margin accounts, positions, perp markets, screener / open-interest, trades, portfolio, applications, delegated-agents, and withdrawals. The vendored spec is now regenerated automatically and drift-gated in CI.
## Changed
### REST and gRPC now share one set of handlers
The market, orders, positions, fees, accounts, and health surfaces are now served by shared handler logic on both transports, with the REST gateway as the source of truth. REST request/response wire shapes are unchanged; the gRPC surface is aligned to match REST — including error codes (business-rule violations return `INVALID_ARGUMENT`), decimal precision, and normalized amounts.
* `GetMarketMetadata` no longer returns live `mark_price` / `index_price`; read live pricing from `getMarkPrice` / `getIndexPrice`.
* gRPC `AuthService` now records the caller's origin / IP / user-agent on the session and enforces the application's allowed-origins list on `Challenge`, matching REST.
### `createDelegatedSession` response
The response no longer includes `access_token` (it never did over the wire). It carries `expires_at`, `delegation_id`, `owner_user_id`, and `agent_address`. The request now requires the agent's `sessionPublicKey` alongside `ownerUserId`.
### User-trade timestamp format
`executed_at` on user trades (`GET /api/v1/accounts/trades`) now serializes with an explicit `+00:00` UTC offset (e.g. `2025-01-15T10:30:00+00:00`) instead of a trailing `Z`. Both denote the same instant; any RFC 3339 / ISO 8601 parser is unaffected.
### Spec and wire-type corrections
The OpenAPI spec and generated wire-types now document fields the REST API has always returned but previously under-described — additional order fields (`quantity_raw`, lifecycle timestamps, `client_order_id`, `quote_volume`, …), user-trade detail fields, and the `order_by` query parameter on `GET /api/v1/orders`. The embedded match-result field `order_status` is renamed to `status`, and the public trade wire shape is now an envelope (`{ data, event_type, trading_pair_id, trading_mode }`). REST JSON responses are byte-for-byte unchanged.
## Upgrade
```bash theme={null}
npm install @0xmonaco/core@0.8.10 @0xmonaco/types@0.8.10 @0xmonaco/react@0.8.10
# or bun
bun add @0xmonaco/core@0.8.10 @0xmonaco/types@0.8.10 @0xmonaco/react@0.8.10
```
# V0.8.11
Source: https://docs.0xmonaco.com/releases/v0.8.11
# Monaco Protocol SDK v0.8.11
This release adds life-to-date market statistics, asset-class categorisation on trading pairs, and direct deposit routing into margin collateral. It also removes the deprecated `CreateConditionalOrder` gRPC RPC and fixes a scoping bug in parent-account collateral transfers.
## Removed
### `CreateConditionalOrder` gRPC RPC removed
The deprecated `CreateConditionalOrder` RPC and its `CreateConditionalOrderRequest` / `CreateConditionalOrderResponse` messages are **removed** from `monaco.api.orders.OrdersService`. The method had been returning `FAILED_PRECONDITION` since standalone conditional TP/SL creation was deprecated in v0.8.7. The Rust gRPC SDK drops the generated `create_conditional_order` client method and the associated types.
**Migration:** attach TP/SL at order placement via `CreateOrder`, or use `AttachPositionTpSl` / `sdk.positions.attachPositionTpSl(...)`. Listing and cancelling conditional orders are unchanged.
## Added
### Asset-class `category` on trading pairs
Every trading pair now carries a `category` field indicating its asset class: `"crypto"`, `"equities"`, or `"commodities"`.
Affected surfaces:
* `TradingPair` (returned by `GET /api/v1/market/pairs`, `GET /api/v1/market/pairs/{trading_pair_id}`, and gRPC `ListTradingPairs` / `GetTradingPair`) gains `category: string`.
* `ScreenerItem` (returned by `GET /api/v1/market/screener` / `GetScreener`) gains `category: string`.
* `ListTradingPairsRequest` and `GetScreenerRequest` accept an optional `category` filter. Invalid values return 400.
Existing pairs are backfilled: `crypto` by default; known equity/RWA markets (NVDA, SPX, AMZN, GOOG, META, TSLA, SPCX) as `equities`. The field is always present and never null.
```typescript theme={null}
// Filter by asset class
const equities = await sdk.market.getPaginatedTradingPairs({ category: "equities" });
const cryptoScreener = await sdk.market.getScreener({ category: "crypto" });
```
### Life-to-date market statistics
New `GET /api/v1/market/stats` (no auth) returns exchange-wide cumulative statistics:
```typescript theme={null}
const stats = await sdk.market.getMarketStats();
// { total_quote_volume_ltd: "123456789.00", total_trade_count_ltd: 98765 }
```
Per-pair life-to-date totals are also added to existing responses:
* `MarketMetadata` (`getMarketMetadata`) gains `total_base_volume_ltd`, `total_quote_volume_ltd` (decimal strings), and `total_trade_count_ltd` (number).
* `ScreenerItem` (`getScreener`) gains `total_quote_volume_ltd` (decimal string) and `total_trade_count_ltd` (number).
All volume fields are decimal strings in display units. Pairs with no trades report `"0"` / `0`, never null. The global endpoint omits base volume intentionally — base quantities are not comparable across different base assets.
### Direct deposit to margin collateral
`vault.deposit()` accepts an optional `target` parameter to route an on-chain deposit straight into margin collateral, skipping the separate `transferCollateralToAutoMarginAccount` step:
```typescript theme={null}
// Spot deposit (default — unchanged)
await sdk.vault.deposit(assetId, amount);
// Route deposit directly into the parent margin account's collateral
await sdk.vault.deposit(assetId, amount, true, "margin");
```
`target` is `"spot"` (default) or `"margin"`. Deposits that cannot be routed to margin (unsupported asset, etc.) fall back to spot automatically.
The encoding is handled by the new `encodeDepositApplicationData()` helper — spot deposits remain byte-identical to before, so existing integrations are unaffected.
The MCP `vault_deposit` tool accepts the same optional `target` input.
## Fixed
### Parent margin account collateral transfers now correctly scope to parent
`marginAccounts.transferCollateralToMarginAccount(parentMarginAccountId, { asset, amount })` and `transferCollateralFromMarginAccount(parentMarginAccountId, { asset, amount })` now stay scoped to the parent account when `tradingPairId` is omitted, even when an active cross-margin bucket exists. Previously the REST API could route a parent-level transfer into an active cross bucket.
Transfer-out from the parent also now correctly honors live collateral when there is a temporary projection lag, instead of rejecting it as isolated bucket collateral.
### `getAvailableCollateral` returns parent account free collateral
`marginAccounts.getAvailableCollateral({ asset })` previously returned 0 for the parent account in certain configurations. It now correctly surfaces the parent margin account's free collateral alongside the bound asset balance.
## Upgrade
```bash theme={null}
npm install @0xmonaco/core@0.8.11 @0xmonaco/types@0.8.11 @0xmonaco/react@0.8.11
# or bun
bun add @0xmonaco/core@0.8.11 @0xmonaco/types@0.8.11 @0xmonaco/react@0.8.11
```
# V0.8.14
Source: https://docs.0xmonaco.com/releases/v0.8.14
# Monaco Protocol SDK v0.8.14
This release restructures the idless margin-collateral API into explicit **parent margin account** and **risk bucket** endpoints, adds a one-shot **batch close** for open positions, exposes **raw total balances**, and introduces the **`fx`** asset class for Pyth-backed FX perps.
## Removed
### Idless "auto margin account" collateral endpoints replaced
The two idless *auto margin account* shortcuts are **removed** and replaced by the explicit parent-margin-account / risk-bucket surface below:
* `marginAccounts.transferCollateralToAutoMarginAccount({ asset, amount })` — `POST /api/v1/margin/collateral/transfer-in`
* `marginAccounts.simulateAutoMarginOrderRisk(request)` — `POST /api/v1/margin/simulate-order-risk`
Both paths now return **404**. The id-based methods (`transferCollateralToMarginAccount(marginAccountId, …)`, `getMarginAccountSummary(marginAccountId, …)`, `simulateOrderRisk(marginAccountId, …)`, etc.) are unchanged.
**Migration:**
```typescript theme={null}
// Before (v0.8.11)
await sdk.marginAccounts.transferCollateralToAutoMarginAccount({ asset, amount });
await sdk.marginAccounts.simulateAutoMarginOrderRisk(request);
// After (v0.8.14) — parent-account collateral, same request shape
await sdk.marginAccounts.transferCollateralToParentMarginAccount({ asset, amount });
await sdk.marginAccounts.simulateParentMarginOrderRisk(request);
```
If you were funding a per-pair (isolated) bucket through the auto shortcut, use the new risk-bucket methods instead — they take an explicit `tradingPairId` (and optional `strategyKey`).
## Added
### Explicit parent-margin-account and risk-bucket collateral API
The idless margin surface is now split along the account model — one **parent margin account** per user/application, with isolated **risk buckets** nested underneath it (a bucket is created or reused per `tradingPairId` + optional `strategyKey`). No account id is required; the parent is resolved automatically from the caller.
| Method | REST | Purpose |
| - | - | - |
| `getParentMarginAccountSummary(params?)` | `GET /api/v1/margin/parent-margin-account` | Parent summary; pass `tradingPairId` to scope to a bucket |
| `transferCollateralToParentMarginAccount({ asset, amount })` | `POST /api/v1/margin/parent-margin-account/collateral/transfer-in` | Fund the parent account |
| `transferCollateralFromParentMarginAccount({ asset, amount })` | `POST /api/v1/margin/parent-margin-account/collateral/transfer-out` | Withdraw from the parent account |
| `getParentMarginAccountMovements(params?)` | `GET /api/v1/margin/parent-margin-account/movements` | Paginated collateral movements |
| `simulateParentMarginOrderRisk(request)` | `POST /api/v1/margin/parent-margin-account/simulate-order-risk` | Pre-trade risk check at the parent level |
| `transferCollateralToRiskBucket({ asset, amount, tradingPairId, strategyKey? })` | `POST /api/v1/margin/risk-buckets/collateral/transfer-in` | Fund an isolated bucket (created if absent) |
| `simulateRiskBucketOrderRisk(request)` | `POST /api/v1/margin/risk-buckets/simulate-order-risk` | Pre-trade risk check for a bucket |
All collateral transfers return `TransferCollateralResponse` (`movement_id`, `margin_account_id`, `asset`, `amount`, `status`, `new_equity`, `new_total_collateral_value`, `new_withdrawable_collateral`). The simulate endpoints return `SimulateOrderRiskResponse` (`accepted`, `reject_reason?`, `equity_after`, `initial_margin_required_after`, `maintenance_margin_required_after`, `free_collateral_after`, `estimated_fee?`, `estimated_liquidation_price?`). All endpoints require user auth.
```typescript theme={null}
// Fund the parent account, then an isolated bucket for a specific pair
await sdk.marginAccounts.transferCollateralToParentMarginAccount({ asset: usdcId, amount: "1000" });
await sdk.marginAccounts.transferCollateralToRiskBucket({
asset: usdcId,
amount: "250",
tradingPairId: btcPerpId,
});
const summary = await sdk.marginAccounts.getParentMarginAccountSummary();
// { equity, free_collateral, withdrawable_collateral, total_position_notional, unrealized_pnl, ... }
```
### Batch close all open positions
New `POST /api/v1/positions/batch-close-all` (user auth) submits a **MARKET reduce-only** close for every open perp position — a one-call "panic close". It is best-effort and partial: each position closes independently and per-position failures are reported rather than aborting the batch.
```typescript theme={null}
// Close everything
const result = await sdk.positions.batchCloseAllPositions();
// Close only one pair, with slippage tolerance
await sdk.positions.batchCloseAllPositions({
tradingPairId: btcPerpId,
slippageToleranceBps: 50,
});
```
Request (`BatchCloseAllRequest`, all optional): `tradingPairId` (limit to one pair; otherwise all open positions), `slippageToleranceBps` (per-close slippage tolerance).
Response (`BatchCloseAllResponse`): `total_requested`, `total_closed` (orders accepted — SUCCESS / PARTIAL / PENDING), `total_failed`, and `results[]` — each a `BatchCloseResult` with `position_id`, optional `close_order_id`, `status`, `submitted_quantity`, and an optional `error` (`{ code, message }`).
Available across `@0xmonaco/core` (`sdk.positions.batchCloseAllPositions`), `@0xmonaco/react` (`usePositions().batchCloseAllPositions`), and the `batch_close_all_positions` MCP tool.
### Raw total balance
Balance responses gain `total_balance_raw` — the total balance (`available + locked`) as a raw integer string in the token's smallest unit, alongside the existing normalized `total_balance`:
* `AccountBalance` (`GET /api/v1/accounts/balances`) and the single-asset balance response (`GET /api/v1/accounts/balances/{asset_id}`).
* `@0xmonaco/react`'s `useUserBalances` preserves `total_balance_raw` across WebSocket balance updates, mirroring the existing normalized-total handling.
The field is purely additive — existing `available_balance` / `locked_balance` / `total_balance` (and their `_raw` counterparts) are unchanged.
### `fx` asset class
Pyth-backed FX perpetual markets add a fourth value to the trading-pair `category` enum: `"fx"` (joining `"crypto"`, `"equities"`, `"commodities"` from v0.8.11). It is accepted as a `category` filter on `getPaginatedTradingPairs` / `getScreener` and returned on `TradingPair` / `ScreenerItem`.
```typescript theme={null}
const fxPairs = await sdk.market.getPaginatedTradingPairs({ category: "fx" });
```
## Changed
### `margin_bucket_*` fields renamed to `risk_bucket_*`
Isolated-bucket fields are renamed from `margin_bucket_*` to `risk_bucket_*` across orders, positions, and margin responses. The gRPC field numbers are unchanged (binary gRPC clients are unaffected), but the **JSON/REST key changes**, so REST and TypeScript SDK consumers must update:
* **Order placement** — `placeLimitOrder` / `placeMarketOrder` options `marginBucketId` → `riskBucketId` and `marginBucketCollateral` → `riskBucketCollateral` (wire: `risk_bucket_id` / `risk_bucket_collateral`). `riskBucketId` routes the order into an existing isolated bucket; `riskBucketCollateral` opens a new one.
* **Positions** — the `Position` response field `margin_bucket_id` → `risk_bucket_id` (`GET /api/v1/positions`, `GET /api/v1/positions/{position_id}`).
* **Margin** — risk-bucket summary rows expose `risk_bucket_id`.
```typescript theme={null}
await sdk.orders.placeMarketOrder({
tradingPairId: btcPerpId,
side: "BUY",
quantity: "0.1",
riskBucketCollateral: "500", // renamed from marginBucketCollateral
});
```
### Withdrawals migrated to merkle-proof `executeWithdrawal`
The withdrawal flow now submits an on-chain **merkle-proof `executeWithdrawal`** transaction, replacing the previous server-signed `executeSignedWithdrawal` path. Because the executable calldata needs the withdrawal's merkle proof — which only exists once the root is confirmed on-chain — the SDK now **polls** `GET /api/v1/withdrawals/{withdrawal_index}` (which returns 404/409 until the proof is ready) before submitting. `sdk.vault.withdraw(assetId, amount, autoWait?)` and `retryWithdrawal(...)` keep their call signatures (new arguments are optional), so most call sites need only a version bump — **but a v0.8.11 SDK uses the old signed-withdrawal flow (immediate `executeSignedWithdrawal`, no proof polling), which no longer works against the v0.8.14 backend and vault contract, so frontends must upgrade to the v0.8.14 SDK.**
On a proof-poll timeout, `withdraw()` / `retryWithdrawal()` no longer throw — they resolve to `{ withdrawalIndex, status: "awaiting_proof" }` with nothing submitted on-chain, so the caller keeps the index and can finish the withdrawal later via `retryWithdrawal(withdrawalIndex)`. `WithdrawResult.status` gains `"awaiting_proof"` alongside `"pending"` / `"confirmed"` / `"failed"`. The default proof-poll timeout is **5 minutes** (override with `retry.timeoutMs`; pass `Infinity` to wait indefinitely).
`sdk.vault.withdraw(assetId, amount, autoWait, "margin")` can also source an external withdrawal directly from the parent margin account; retry options remain supported as the legacy fourth argument.
### Address-shaped return types are now viem `Address`
Public SDK methods that return addresses now type them as viem's `Address` (`0x${string}`) instead of `string`, so they flow into viem calls without a cast. `SDK.getAccountAddress()` returns `Address` and `vault.getVaultAddress()` returns `Promise`; address fields on returned response/event objects (account / profile / balances, applications config & reporting, market trading-pair contracts, delegated agents, vault balances, withdrawals, and the user balance/movement WebSocket events) are likewise typed as `Address`. This is a **type-only** change — no runtime behavior changes, and request/input address params remain `string`.
## Upgrade
```bash theme={null}
npm install @0xmonaco/core@0.8.14 @0xmonaco/types@0.8.14 @0xmonaco/react@0.8.14
# or bun
bun add @0xmonaco/core@0.8.14 @0xmonaco/types@0.8.14 @0xmonaco/react@0.8.14
```
# V0.8.15
Source: https://docs.0xmonaco.com/releases/v0.8.15
# Monaco Protocol SDK v0.8.15
This release switches the **REST API wire format from snake\_case to camelCase** across every endpoint, and adds a **`GET /api/v1/withdrawals`** endpoint for listing the caller's pending withdrawals.
## Breaking
### REST wire format is now camelCase
Every REST request/response JSON field **and** path parameter is now **camelCase** instead of snake\_case. This is a wire-format change that spans the entire REST surface — for example `trading_pair_id` → `tradingPairId`, `order_id` → `orderId`, `asset_id` → `assetId`, `created_at` → `createdAt`, `page_size` → `pageSize`, `sub_account_id` → `subAccountId`, and path params such as `/withdrawals/{withdrawal_index}` → `/withdrawals/{withdrawalIndex}`.
**If you use the TypeScript SDK** (`@0xmonaco/core`, `@0xmonaco/react`), it already speaks camelCase end to end — upgrade to v0.8.15 and no field-name changes are needed in your code.
**If you call the REST API directly** (raw HTTP, or raw JSON via the Rust REST SDK), rename every field and path parameter to camelCase:
```jsonc theme={null}
// Before (v0.8.14) — POST /api/v1/orders
{ "trading_pair_id": "…", "order_type": "LIMIT", "side": "BUY", "quantity": "0.1" }
// After (v0.8.15) — POST /api/v1/orders
{ "tradingPairId": "…", "orderType": "LIMIT", "side": "BUY", "quantity": "0.1" }
```
The **WebSocket API is unchanged.** Its raw wire payloads, event types, and channel names remain snake\_case (`trade`, `orderbook_update`, `positions:{trading_pair_id}`, …). The TypeScript SDK's WebSocket hooks continue to surface camelCase fields to your code as before. **gRPC is also unaffected** — binary field numbers and proto field names are unchanged.
## Added
### List pending withdrawals
New `GET /api/v1/withdrawals` (user auth) returns the caller's **pending** withdrawals — those still awaiting on-chain root confirmation, whose merkle proof (and therefore the executable calldata from `GET /api/v1/withdrawals/{withdrawalIndex}`) is not available yet. Scoped to the authenticated user + application, newest first, and paginated.
```typescript theme={null}
const { withdrawals, total, page, pageSize, totalPages } =
await sdk.withdrawals.listPendingWithdrawals({ pageSize: 50 });
for (const w of withdrawals) {
// { withdrawalIndex, assetId, assetSymbol, amount, destination, status: "pending", createdAt }
console.log(w.withdrawalIndex, w.assetSymbol, w.amount);
}
```
Query params (`ListPendingWithdrawalsParams`, all optional): `page` (1-based, default 1), `pageSize` (1–100, default 20).
Response (`ListPendingWithdrawalsResponse`): `withdrawals[]` — each a `PendingWithdrawal` with `withdrawalIndex`, `assetId`, `assetSymbol`, `amount` (raw smallest-unit string), `destination`, `status` (always `"pending"`), and `createdAt` — plus `total`, `page`, `pageSize`, and `totalPages`.
Available across `@0xmonaco/core` (`sdk.withdrawals.listPendingWithdrawals`) and `@0xmonaco/react` (`useVault().listPendingWithdrawals`).
## Upgrade
```bash theme={null}
npm install @0xmonaco/core@0.8.15 @0xmonaco/types@0.8.15 @0xmonaco/react@0.8.15
# or bun
bun add @0xmonaco/core@0.8.15 @0xmonaco/types@0.8.15 @0xmonaco/react@0.8.15
```
# V0.8.16
Source: https://docs.0xmonaco.com/releases/v0.8.16
# Monaco Protocol SDK v0.8.16
This release corrects **portfolio statistics**: `pnl` now reports true realized PnL (previously it was net trading cash-flow), account equity now includes perpetuals, and the stats response gains a spot/perps equity breakdown. Affects `GET /api/v1/accounts/me/portfolio` (`sdk.profile.getPortfolioStats`) and the `pnl` metric of the portfolio chart (`sdk.profile.getPortfolioChart(period, "pnl")`).
## Fixed
### Realized PnL
`getPortfolioStats().pnl` and `getPortfolioChart(period, "pnl")` now return **average-cost realized PnL** — gains and losses are booked only when a position is reduced or closed, net of fees, across spot and perps (including funding), and exclude unrealized PnL on open positions.
Previously `pnl` summed the quote-currency (USDC) leg of trade ledger entries — i.e. net trading cash-flow — so **buying and holding** an asset showed as a loss and **selling deposited inventory** showed as pure profit. The response shape is unchanged; only the values (and the field documentation) change.
### `winLossRatio` type
`PortfolioStats.winLossRatio` is now typed **`number | null`** (previously `string`), matching the value the API actually returns. It is `null` when the period has no closed trades.
## Added
### Spot / perps equity breakdown
`PortfolioStats` gains two additive fields alongside `totalEquity`:
* **`spotEquity`** — spot account equity (quote-normalized string).
* **`perpsEquity`** — perpetuals (margin) account equity: collateral + realized − funding + unrealized PnL (quote-normalized string). Previously this was effectively `0`.
## Changed
### `totalEquity` now includes perpetuals
`totalEquity` now reflects **spot balances plus perpetuals (margin) account equity**, not spot balances alone.
### `maxDrawdown` nullable
`PortfolioStats.maxDrawdown` is now **`string | null`** — `null` when the period has no closed trades.
## Upgrade
```bash theme={null}
npm install @0xmonaco/core@0.8.16 @0xmonaco/types@0.8.16 @0xmonaco/react@0.8.16
# or bun
bun add @0xmonaco/core@0.8.16 @0xmonaco/types@0.8.16 @0xmonaco/react@0.8.16
```
Aside from the additive `spotEquity` / `perpsEquity` fields and the `winLossRatio` / `maxDrawdown` type refinements, request and response shapes are unchanged — most callers only see corrected values and need no code changes.
# V0.8.18
Source: https://docs.0xmonaco.com/releases/v0.8.18
# Monaco Protocol SDK v0.8.18
This release adds a **fee-tier lookup** and corrects **spot equity valuation** in portfolio stats. The new `GET /api/v1/fees/tier` endpoint (`sdk.fees.getMyFeeTier`) returns your current volume-based fee tier, your rolling 14-day volumes, how much more volume reaches the next tier, and a trading pair's full six-row fee schedule — with a matching React hook and MCP tool.
## Fixed
### `spotEquity` valued at mark price
`GET /api/v1/accounts/me/portfolio` (`sdk.profile.getPortfolioStats`) now marks each spot balance to its current price before summing, instead of adding up raw token quantities as if every asset were worth one dollar. Previously `spotEquity` could be materially wrong in both directions — assets trading above a dollar were undercounted while sub-dollar assets were overcounted. `totalEquity` (which includes `spotEquity`) is corrected accordingly, and assets with no active USDC-quoted pair are valued at `0`. The response shape is unchanged; only the values change.
## Added
### `getMyFeeTier` — query your fee tier
Monaco charges volume-based [maker/taker fees](/fees) across six tiers, recalculated daily from a trailing 14-day weighted volume (`14d perp + 2.5 × 14d spot`). The new endpoint returns, for the authenticated caller:
* **`currentTierLevel`** — resolved tier, `1` (lowest volume) through `6` (highest).
* **`weightedVolume14d`** — weighted 14-day volume used to resolve the tier.
* **`spotVolume14d`** / **`perpVolume14d`** — the rolling 14-day spot and perp volume components.
* **`volumeToNextTier`** / **`nextTierLevel`** — additional weighted volume needed to reach the next tier and its level; both omitted once you are at tier 6.
* **`feeSchedule`** — the requested pair's six rows (`tierLevel`, `minVolumeThreshold`, `makerFeeBps`, `takerFeeBps`), ascending by tier. Fees are in human basis points; a negative `makerFeeBps` is a rebate.
**Surfaces**
* REST: `GET /api/v1/fees/tier?tradingPairId=` (authenticated)
* Core SDK: `sdk.fees.getMyFeeTier({ tradingPairId })`
* React: `useFees().getMyFeeTier({ tradingPairId })`
* MCP: `get_my_fee_tier` tool
* gRPC: `FeesService.GetMyFeeTier`
## Upgrade
```bash theme={null}
npm install @0xmonaco/core@0.8.18 @0xmonaco/types@0.8.18 @0xmonaco/react@0.8.18
# or bun
bun add @0xmonaco/core@0.8.18 @0xmonaco/types@0.8.18 @0xmonaco/react@0.8.18
```
This release is additive — existing request and response shapes are unchanged, and no code changes are required to upgrade.
# V0.8.19
Source: https://docs.0xmonaco.com/releases/v0.8.19
# Monaco Protocol SDK v0.8.19
This release adds **cross-margin support** to Monaco's risk-bucket margin system. Margin orders can now be placed against a shared **cross** risk bucket that spans several trading pairs, instead of only per-pair **isolated** risk buckets. The SDK surfaces the resolved margin mode and the cross bucket's selected-pair scope across order placement, positions, margin-account reads, collateral transfers, and risk simulation.
## Added
### Cross margin on risk buckets
Risk buckets now come in two modes, surfaced everywhere as `marginMode`:
* **`ISOLATED`** — a per-pair risk bucket keyed by `tradingPairId`. This is the default and existing behavior.
* **`CROSS`** — a shared risk bucket whose collateral backs positions across a set of `selectedTradingPairIds`.
A new `MarginMode` type (`"ISOLATED" | "CROSS"`) is exported for reuse.
**Order placement** — `placeLimitOrder`, `placeMarketOrder`, and `batchCreate` accept an optional `marginMode` (defaults to `ISOLATED`). Passing `"CROSS"` requires `marginAccountId`, routes the order to the active cross risk bucket for the selected `tradingPairId`, and cannot be combined with `riskBucketId`, `riskBucketCollateral`, or `strategyKey`.
**Positions** — `Position` (list and get) now includes an optional `marginMode`, so you can distinguish cross positions from isolated ones.
**Margin-account reads** — `MarginAccountSummary` (list and summary) now includes `selectedTradingPairIds` on cross risk-bucket rows, so a client can restore the cross bucket's selected-pair scope from account reads alone.
**Collateral transfer** — `transferCollateralToRiskBucket` now takes `marginMode`. For `CROSS`, pass `selectedTradingPairIds` and omit `tradingPairId` and `strategyKey`; for `ISOLATED` (default), pass `tradingPairId` as before. The response echoes the resolved `riskBucketId`, `marginMode`, and `selectedTradingPairIds`.
**Risk simulation** — `simulateRiskBucketOrderRisk` accepts the same `marginMode` and `selectedTradingPairIds` inputs, and its response now includes the resolved `riskBucketId`, `marginMode`, and `selectedTradingPairIds`.
**Surfaces**
* REST: `marginMode` on `POST /api/v1/orders` and `POST /api/v1/orders/batch-create`; `marginMode` + `selectedTradingPairIds` on `POST /api/v1/margin/risk-buckets/collateral/transfer-in` and `POST /api/v1/margin/risk-buckets/simulate-order-risk`; `marginMode` on position reads (`GET /api/v1/positions`, `GET /api/v1/positions/{positionId}`); `selectedTradingPairIds` on margin-account list and summary reads
* Core SDK: `sdk.trading.placeLimitOrder` / `placeMarketOrder` / `batchCreate`, `sdk.marginAccounts.transferCollateralToRiskBucket` / `simulateRiskBucketOrderRisk`
* React: the same contracts flow through the `@0xmonaco/react` hooks via the shared core/types surface
* gRPC: `CreateOrderRequest`, `BatchCreateOrderItem`, `Position`, `MarginAccountSummary`, and the risk-bucket transfer and simulation messages gain the new fields
## Upgrade
```bash theme={null}
npm install @0xmonaco/core@0.8.19 @0xmonaco/types@0.8.19 @0xmonaco/react@0.8.19
# or bun
bun add @0xmonaco/core@0.8.19 @0xmonaco/types@0.8.19 @0xmonaco/react@0.8.19
```
This release is additive — `marginMode` defaults to `ISOLATED`, existing request and response shapes are unchanged, and no code changes are required to upgrade.
# V0.8.20
Source: https://docs.0xmonaco.com/releases/v0.8.20
# Monaco Protocol SDK v0.8.20
This release improves cross-margin order placement, hardens fee simulation accuracy, and fixes three correctness bugs: stale balances after spot fills, collateral not freed after a downward order replace, and an incorrect error code on unauthorized sub-account spending-limit reads.
## Changed
### Cross margin auto-resolved on order placement
`POST /api/v1/orders` and `POST /api/v1/orders/batch-create` with `marginMode: "CROSS"` now automatically provision or expand the cross risk bucket without any extra setup step:
* **No active cross bucket** — the API resolves the deterministic cross risk bucket, estimates the required order collateral, and forwards the bucket and collateral amount to matching-engine admission automatically.
* **Active cross bucket missing the requested pair** — the API adds the `tradingPairId` to the cross bucket's scope before forwarding the order.
No request or response fields changed. Existing `marginMode: "ISOLATED"` flows and cross orders placed against a fully-configured bucket are unaffected.
### Sub-account spending limit unauthorized read returns 404
`GET /api/v1/accounts/sub-accounts/{subAccountId}/limits` now returns **404 Not Found** (gRPC `NOT_FOUND`) when the sub-account belongs to a different master, instead of **403 Forbidden** (`PERMISSION_DENIED`). This matches the behavior of the create, update, and delete limit RPCs and makes an unauthorized read indistinguishable from a nonexistent sub-account. No request or response fields changed.
## Fixed
### `balances` WebSocket missing spot-fill updates
`sdk.ws.balances()` (WebSocket channel `balances`) now emits a `trade`-reason balance update to both the maker and taker when a spot order fills. Previously the channel only pushed updates on deposits, withdrawals, and collateral transfers, so live balance state went stale after spot trading. No subscription or message-shape changes; the update now arrives where it was already documented to arrive.
### `simulateFees` used flat fee rate instead of your tier rate
`sdk.trading.simulateFees` now quotes your **tier-resolved** taker fee — the rate the ledger actually debits — instead of the trading pair's flat fee column. The affected response fields are `monacoTakerFee`, `totalTakerFees`, and `takerTotalPayment`. If your account is on a tier whose rate exceeds the flat column the estimate was previously under-quoted; no request or response fields changed.
### Isolated margin replace to lower notional left collateral stale
Replacing an isolated margin order with a lower notional now immediately returns the freed collateral delta to the parent account. The following reads now reflect the correct `freeCollateral` right after the replace:
* `sdk.marginAccounts.getMarginAccountSummary`
* `sdk.marginAccounts.getParentMarginAccountSummary`
* `sdk.marginAccounts.getAvailableCollateral`
Previously the risk bucket's allocated collateral was not adjusted on replace, so `freeCollateral` stayed at its pre-replace value until the order was eventually cancelled or filled.
## Upgrade
```bash theme={null}
npm install @0xmonaco/core@0.8.20 @0xmonaco/types@0.8.20 @0xmonaco/react@0.8.20
# or bun
bun add @0xmonaco/core@0.8.20 @0xmonaco/types@0.8.20 @0xmonaco/react@0.8.20
```
This release is fully backwards-compatible — no request or response fields changed across all five fixes and improvements.
# V0.8.21
Source: https://docs.0xmonaco.com/releases/v0.8.21
# Monaco Protocol SDK v0.8.21
This release adds a new endpoint for fetching a single conditional (TP/SL) order by ID, ships a substantial update to realized-PnL accounting semantics, and fixes three correctness bugs: taker fee fields on filled orders reporting the wrong rate, cross-margin simulation failing before the first cross order, and ambiguous parent margin-account orders silently routing through the wrong path. The `conditional_orders` WebSocket channel now also gates on authentication at subscribe time.
## Added
### Get a conditional order by ID
A new endpoint fetches a single TP/SL conditional order directly by its UUID:
* **REST** — `GET /api/v1/orders/conditional/{conditionalOrderId}`
* **TypeScript SDK** — `sdk.trading.getConditionalOrder(conditionalOrderId)`
* **React SDK** — `useTrade().getConditionalOrder(conditionalOrderId)`
* **gRPC** — `OrdersService.GetConditionalOrder`
The returned `ConditionalOrder` is the same type produced by `listConditionalOrders`. The `takeProfitOrderId` and `stopLossOrderId` fields on a `createOrder` response are conditional order UUIDs queryable via this method.
```typescript theme={null}
const order = await sdk.trading.placeLimitOrder(pair.id, "BUY", "1.5", "2000", {
tradingMode: "MARGIN",
positionSide: "LONG",
leverage: "10",
takeProfit: { triggerPrice: "2200", orderType: "MARKET" },
});
// Look up the attached TP leg by ID
const tp = await sdk.trading.getConditionalOrder(order.takeProfitOrderId);
console.log(tp.state, tp.triggerPrice);
```
## Changed
### Realized PnL accounting semantics
`getPortfolioStats` (`pnl`) and `getPortfolioChart` (the `"pnl"` metric) now apply updated realized-PnL semantics as part of the unrealized-PnL rollout:
* **Spot withdrawals are fair-value disposals** — withdrawing a spot asset books `(last trade price − average cost) × quantity` as realized PnL, rather than treating the outflow as cash-neutral.
* **Deposits and withdrawals use last trade price** — cost-basis entries and disposal prices are now pinned to the last observed trade price for the pair in event order, instead of the covering 1-minute candle close.
* **Pre-first-trade transfers are PnL-neutral** — any deposit or withdrawal that occurs before the first trade on a pair does not generate realized PnL.
* **Short-entry fees reduce short profits** — fees paid on short position entry now reduce the profit of that short, rather than inflating it.
Response shape is unchanged; the `pnl` values will reflect the corrected accounting going forward.
## Fixed
### Taker order fee fields reported flat rate instead of tier rate
A filled order's fee response fields — `monacoTakerFee`, `totalTakerFees`, and `takerTotalPayment` — previously reported the trading pair's flat fee column rather than the tier-resolved rate that was actually debited at settlement. These fields now match the balance movement. Clients on a fee tier whose rate differs from the flat column will see corrected values; no request or response fields changed.
### Cross-margin simulation failed before the first cross order
`POST /api/v1/margin/risk-buckets/simulate-order-risk` (`sdk.marginAccounts.simulateRiskBucketOrderRisk`) with `marginMode: "CROSS"` previously returned an error when the user had no active cross risk bucket. The endpoint now uses the same deterministic cross risk bucket ID and first-use auto-collateral estimate as live order placement, so you can safely preflight a cross order before the bucket exists. No request or response fields changed.
### Ambiguous parent margin-account orders now fail fast
`POST /api/v1/orders` and `POST /api/v1/orders/batch-create` for a parent margin-account order that omits `marginMode` and does not provide an isolated risk-bucket selector previously silently routed through the isolated risk-bucket path. These requests now return a client error. Pass `marginMode: "CROSS"` or `marginMode: "ISOLATED"` for all parent margin-account margin orders. No request or response fields changed.
## Security
### `conditional_orders` WebSocket channel now requires authentication at subscribe time
The `conditional_orders` WebSocket channel (`sdk.ws.conditionalOrders`) now requires authentication when subscribing, matching the behavior of all other user-scoped channels (`orders`, `balances`, `positions`, `movements`, `liquidations`). Event delivery was already owner-scoped — this closes the subscribe-gate gap. No subscription API changes; unauthenticated subscribe attempts are now rejected.
## Upgrade
```bash theme={null}
npm install @0xmonaco/core@0.8.21 @0xmonaco/types@0.8.21 @0xmonaco/react@0.8.21
# or bun
bun add @0xmonaco/core@0.8.21 @0xmonaco/types@0.8.21 @0xmonaco/react@0.8.21
```
The only breaking-adjacent change in this release is the `conditional_orders` subscribe gate: unauthenticated subscribers will now receive a rejection instead of connecting successfully. Ensure your app authenticates before calling `sdk.ws.conditionalOrders(...)`. All other changes are additive or behavioral corrections with no shape changes.
# V0.8.22
Source: https://docs.0xmonaco.com/releases/v0.8.22
# Monaco Protocol SDK v0.8.22
This release ships the full unrealized-PnL surface — new `unrealizedPnl`, `totalPnl`, and `pnlBreakdown` fields on portfolio stats, six new chart metrics, and a new per-position PnL history endpoint — adds `quantityStepSize` to trading pairs, and fixes position endpoints to return position-scoped margin requirements instead of the parent-account aggregate.
## Added
### Quantity step size on trading pairs
`TradingPair` now exposes `quantityStepSize` (string): the base-token lot-size increment used to validate order quantity precision. Available on all trading-pair reads:
* **REST** — `GET /api/v1/market/pairs`, `GET /api/v1/market/pairs/{tradingPairId}`
* **TypeScript SDK** — `market.getPaginatedTradingPairs()`, `market.getTradingPair(tradingPairId)`
* **gRPC** — `MarketService.ListTradingPairs`, `MarketService.GetTradingPair`
`minOrderSize` values that were below the provider or base-asset lot boundary have been raised to the next executable lot. `maxOrderSize` and `tickSize` semantics are unchanged.
```typescript theme={null}
const pair = await sdk.market.getTradingPair(tradingPairId);
console.log(pair.quantityStepSize); // e.g. "0.001"
```
### Expanded PnL surface
#### Portfolio stats — unrealized PnL and breakdown
`getPortfolioStats` returns three new fields:
| Field | Type | Description |
| - | - | - |
| `unrealizedPnl` | string | Mark-to-market PnL on all open positions |
| `totalPnl` | string | `realizedPnl + unrealizedPnl − fundingPaid − fees` |
| `pnlBreakdown` | object | Component breakdown (see below) |
`pnlBreakdown` contains: `spotRealized`, `spotUnrealized`, `perpsRealized`, `perpsUnrealized`, `fundingPaid`, `fees`. `fundingPaid` is cost-positive (positive means paid out); negate it when displaying as a PnL contribution.
#### Portfolio chart — new metrics
`getPortfolioChart` now accepts six additional metric values: `"unrealized_pnl"`, `"total_pnl"`, `"realized_pnl"`, `"funding_paid"`, `"fees"`, `"equity"`. Each is served from PnL history snapshots with per-bucket forward-fill.
```typescript theme={null}
const chart = await sdk.profile.getPortfolioChart("1D", "total_pnl");
```
#### Position PnL history
A new endpoint returns bucketed PnL state samples for a single owned position:
* **REST** — `GET /api/v1/positions/{positionId}/pnl/history?interval=&startTime=&endTime=`
* **TypeScript SDK** — `positions.getPositionPnlHistory(positionId, { interval, startTime?, endTime? })`
* **React SDK** — `usePositionPnlHistory(positionId, { interval, startTime?, endTime? })`
* **gRPC** — `PositionsService.GetPositionPnlHistory`
`interval` accepts: `"1m"`, `"5m"`, `"15m"`, `"1h"`, `"4h"`, `"1d"`. Returns an array of `PositionPnlPoint` samples forward-filled within the requested window.
```typescript theme={null}
const history = await sdk.positions.getPositionPnlHistory(positionId, {
interval: "1h",
startTime: "2026-07-01T00:00:00Z",
});
for (const point of history.points) {
console.log(point.timestamp, point.unrealizedPnl, point.realizedPnl);
}
```
## Fixed
### Per-position `initialMarginRequired` and `maintenanceMarginRequired`
`listPositions`, `getPosition`, and `getPositionRisk` now return values scoped to the individual position rather than repeating the parent margin-account aggregate. Initial margin honors effective leverage and the market initial-margin floor; maintenance margin uses the market maintenance-margin rate. Positions with no open exposure return `"0"` for both fields. Request and response field names, types, and authentication requirements are unchanged.
## Upgrade
```bash theme={null}
npm install @0xmonaco/core@0.8.22 @0xmonaco/types@0.8.22 @0xmonaco/react@0.8.22
# or bun
bun add @0xmonaco/core@0.8.22 @0xmonaco/types@0.8.22 @0xmonaco/react@0.8.22
```
All changes in this release are additive or behavioral corrections with no field-shape changes.
# V0.8.4
Source: https://docs.0xmonaco.com/releases/v0.8.4
# Monaco Protocol SDK v0.8.4
## Delegated Agents
Delegated agents let an owner wallet authorize a separate signer address to submit orders on
the owner's behalf. The owner retains custody of all funds, collateral movement, and withdrawal
rights. The agent can only perform the trading actions permitted by its policy.
### How it works
1. **Owner** calls `upsertDelegatedAgent` to register an agent address and define its constraints.
2. **Agent** authenticates with its own wallet (normal `sdk.auth` challenge flow), then calls
`createDelegatedSession` with the owner's user ID to receive a short-lived, owner-scoped
delegated auth session.
3. **Agent** signs trading requests with that delegated session. Orders are matched and settled
against the owner's margin accounts; the agent wallet is recorded as the signer.
### Owner setup
```typescript theme={null}
const agent = await sdk.delegatedAgents.upsertDelegatedAgent({
agentAddress: "0xYourAgentWalletAddress",
allowedActions: ["CREATE_ORDER", "CANCEL_ORDER", "REPLACE_ORDER"],
allowedTradingPairIds: [btcPerpTradingPairId],
allowedOrderTypes: ["LIMIT"],
allowedTimeInForce: ["GTC", "IOC"],
maxLeverage: "10",
maxOrderNotional: "100000",
expiresAt: "2026-12-31T23:59:59Z",
});
console.log(agent.id); // UUID — store for revocation
```
### Agent session
The agent first authenticates with its own wallet, then exchanges for an owner-scoped delegated
session using a locally generated session public key:
```typescript theme={null}
// Agent is already authenticated with its own wallet address
const session = await sdk.delegatedAgents.createDelegatedSession({
ownerUserId: ownerUserId,
sessionPublicKey,
});
// From here, signed delegated requests act on the owner's account
// while preserving the agent address for policy checks and audit.
```
### Placing orders as an agent
Once the delegated session is active, place orders normally. Pass `strategyKey` to let Monaco
auto-resolve or create an isolated margin bucket for that key, so you never need to manage
`marginAccountId` directly:
```typescript theme={null}
await sdk.trading.placeLimitOrder(btcPerpTradingPairId, "BUY", "0.5", "65000", {
tradingMode: "MARGIN",
positionSide: "LONG",
leverage: "5",
strategyKey: "btc-mm-strategy-a",
});
```
Each unique `strategyKey` maps to a dedicated, owner-owned margin account. Omitting `strategyKey`
uses the owner's default margin account for the trading pair.
### List and revoke
```typescript theme={null}
// List all active delegated agents for the authenticated owner
const { agents } = await sdk.delegatedAgents.listDelegatedAgents();
// Revoke a specific agent
const result = await sdk.delegatedAgents.revokeDelegatedAgent(agent.id);
// result.status === "REVOKED"
```
Revoking an agent blocks future policy-gated trading for that agent. Existing delegated auth
sessions may still authenticate until session expiry, but create, cancel, replace, and close
actions fail because Monaco re-checks the active delegation before each mutation. In-flight orders
already submitted are unaffected.
### `UpsertDelegatedAgentRequest` shape
| Field | Type | Required | Description |
| - | - | - | - |
| `agentAddress` | string | yes | EVM wallet address of the agent |
| `allowedActions` | `DelegatedAgentAction[]` | yes | `"CREATE_ORDER"`, `"CANCEL_ORDER"`, `"REPLACE_ORDER"` |
| `allowedTradingPairIds` | string\[] | no | Restrict to specific trading pair UUIDs; omit to allow all |
| `allowedOrderTypes` | string\[] | no | e.g. `["LIMIT"]`; omit to allow all |
| `allowedTimeInForce` | string\[] | no | e.g. `["GTC", "IOC"]`; omit to allow all |
| `maxLeverage` | string | no | Maximum leverage the agent may request, e.g. `"10"` |
| `maxOrderNotional` | string | no | Maximum notional per order in quote asset units |
| `expiresAt` | string | no | ISO 8601 expiry — agent policy auto-expires at this time |
***
## `strategyKey` on order placement
Both `placeLimitOrder` and `placeMarketOrder` now accept an optional `strategyKey` field. Monaco
uses this to resolve the owner's parent margin account and target the isolated bucket for the
given (market, strategyKey) pair. This is the recommended way for delegated agents to trade
without managing bucket IDs manually — but it is also usable in non-delegated flows for strategy
tagging and segregated margin accounting.
```typescript theme={null}
await sdk.trading.placeLimitOrder(tradingPairId, "SELL", "1.0", "3000", {
tradingMode: "MARGIN",
positionSide: "SHORT",
leverage: "3",
strategyKey: "eth-trend-follow",
});
```
`strategyKey` is also available on `batchCreate` and `batchReplace` per-order params.
***
## Migration
No breaking changes. All additions are opt-in:
* `sdk.delegatedAgents` is a new module — existing code is unaffected.
* `strategyKey` is optional — all existing order placement calls work without change.
***
## Upgrade
```bash theme={null}
npm install @0xmonaco/core@0.8.4 @0xmonaco/types@0.8.4 @0xmonaco/react@0.8.4
# or bun
bun add @0xmonaco/core@0.8.4 @0xmonaco/types@0.8.4 @0xmonaco/react@0.8.4
```
# V0.8.5
Source: https://docs.0xmonaco.com/releases/v0.8.5
# Monaco Protocol SDK v0.8.5
## App Statistics (Backend)
Backend users can now fetch aggregate volume and fee metrics for their application via a new endpoint available on both REST and gRPC. Auth requires a backend secret key (`sk_...`).
### REST
```
GET /api/v1/applications/stats
Authorization: Bearer sk_...
```
**Query parameters:**
| Param | Type | Required | Description |
| - | - | - | - |
| `since` | string (ISO 8601) | no | Filter stats from this datetime, e.g. `2026-01-01T00:00:00Z`. Omit for all-time |
**Response (`GetAppStatsResponse`):**
| Field | Type | Description |
| - | - | - |
| `volume` | string | Total quote volume for trades where this application's users were the taker |
| `maker_fee` | string | Total maker fees collected |
| `taker_fee` | string | Total taker fees collected |
| `application_taker_fee` | string | Application revenue share from taker fees |
| `trade_count` | number | Total number of trades |
All monetary values are normalized, human-readable strings in quote token units (e.g. `"12345.67"`).
### TypeScript SDK
```typescript theme={null}
// Requires backend SDK initialization (secret key)
const stats = await sdk.applications.getApplicationStats();
// With time filter:
const statsFrom = await sdk.applications.getApplicationStats({
since: "2026-01-01T00:00:00Z",
});
console.log(stats.volume); // "12345.67"
console.log(stats.application_taker_fee); // "5.67"
console.log(stats.trade_count); // 42
```
### gRPC
RPC: `ApplicationsService/GetAppStats`
```protobuf theme={null}
message GetAppStatsRequest {
optional string since = 1; // ISO 8601, e.g. "2026-01-01T00:00:00Z"
}
message GetAppStatsResponse {
string volume = 1;
string maker_fee = 2;
string taker_fee = 3;
string application_taker_fee = 4;
int64 trade_count = 5;
}
```
Auth: `x-server-key: sk_...` header.
***
## Auto-Margin Funding & Risk Simulation
Two new `sdk.marginAccounts` methods let you fund and simulate orders against an auto-resolved margin bucket, using a `(tradingPairId, strategyKey)` pair instead of a raw `marginAccountId`. This complements the `strategyKey` introduced in v0.8.4 for delegated agents and strategy-segregated accounts.
### `transferCollateralToAutoMarginAccount`
Transfers collateral into the margin bucket for a given `tradingPairId` + optional `strategyKey`. Monaco resolves the parent account and ensures the bucket exists automatically.
```typescript theme={null}
await sdk.marginAccounts.transferCollateralToAutoMarginAccount({
asset: "USDC",
amount: "500",
tradingPairId: btcPerpTradingPairId,
strategyKey: "btc-mm-strategy-a", // optional
});
```
**Parameters (`TransferCollateralToAutoMarginAccountRequest`):**
| Field | Type | Required | Description |
| - | - | - | - |
| `asset` | string | yes | Collateral asset, e.g. `"USDC"` |
| `amount` | string | yes | Decimal amount to transfer in |
| `tradingPairId` | string (UUID) | yes | Trading pair the margin account is scoped to |
| `strategyKey` | string | no | Strategy tag — omit to target the default account for the pair |
Response shape is the same as `transferCollateralToMarginAccount` (`movement_id`, `margin_account_id`, `new_equity`, `new_total_collateral_value`, `new_withdrawable_collateral`).
### `simulateAutoMarginOrderRisk`
Preflight a perp order against the auto-resolved margin account for a `(tradingPairId, strategyKey)` — no `marginAccountId` lookup required.
```typescript theme={null}
const risk = await sdk.marginAccounts.simulateAutoMarginOrderRisk({
tradingPairId: btcPerpTradingPairId,
strategyKey: "btc-mm-strategy-a", // optional
side: "BUY",
positionSide: "LONG",
orderType: "LIMIT",
price: "65000",
quantity: "0.5",
leverage: "10",
});
if (!risk.accepted) {
showError(risk.reject_reason);
}
```
**Parameters (`SimulateOrderRiskRequest`):**
| Field | Type | Required | Description |
| - | - | - | - |
| `tradingPairId` | string (UUID) | yes | Trading pair |
| `strategyKey` | string | no | Strategy key — omit for the default account |
| `side` | `"BUY"` \| `"SELL"` | yes | |
| `positionSide` | `"LONG"` \| `"SHORT"` \| `"NONE"` | yes | |
| `orderType` | `"LIMIT"` \| `"MARKET"` | yes | |
| `price` | string | for LIMIT | Required when `orderType` is `"LIMIT"` |
| `quantity` | string | yes | |
| `leverage` | string | yes | e.g. `"10"` |
| `reduceOnly` | boolean | no | |
Response shape is identical to `simulateOrderRisk`.
***
## `listMarginAccounts` — `tradingPairId` filter
`listMarginAccounts` now accepts an optional `tradingPairId` param to filter results to accounts scoped to a specific trading pair.
```typescript theme={null}
const { accounts } = await sdk.marginAccounts.listMarginAccounts({
tradingPairId: btcPerpTradingPairId,
});
```
`MarginAccountSummary` also now includes `trading_pair_id?: string` and `strategy_key?: string` in its response shape.
***
## Upgrade
```bash theme={null}
npm install @0xmonaco/core@0.8.5 @0xmonaco/types@0.8.5 @0xmonaco/react@0.8.5
# or bun
bun add @0xmonaco/core@0.8.5 @0xmonaco/types@0.8.5 @0xmonaco/react@0.8.5
```
# V0.8.7
Source: https://docs.0xmonaco.com/releases/v0.8.7
# Monaco Protocol SDK v0.8.7
## `withdrawable_collateral` now reflects what can actually be moved
`MarginAccountSummary.withdrawable_collateral` and the `new_withdrawable_collateral` field returned by `transferCollateralToMarginAccount` / `transferCollateralFromMarginAccount` now return the risk-bounded free collateral amount instead of equity-based `available_margin`.
Previously the field could overstate the withdrawable amount when an account had realized PnL gains or lower live free collateral, because equity-based `available_margin` includes PnL. Using the field as the "Max" value for `transferCollateralFromMarginAccount` could fail risk validation.
```typescript theme={null}
const summary = await sdk.marginAccounts.getMarginAccountSummary(marginAccountId);
// summary.withdrawable_collateral is now safe to pre-fill into a "Max" button
await sdk.marginAccounts.transferCollateralFromMarginAccount(marginAccountId, {
asset: "USDC",
amount: summary.withdrawable_collateral,
});
```
Affects: `getMarginAccountSummary`, `listMarginAccounts`, `transferCollateralToMarginAccount`, `transferCollateralFromMarginAccount`.
***
## Transfer-out and isolated margin reduction use margin state directly
`transferCollateralFromMarginAccount` no longer depends on wallet-side mirrored margin collateral. Transfer-out validation uses the margin account's free collateral and collateral asset binding directly.
The same model applies to `sdk.positions.reducePositionMargin`: isolated margin reduction uses margin position/account state and credits spot wallet balance through the transfer ledger.
No SDK signature changes — affected calls just succeed where they used to error.
***
## `latest_orders` and `latest_movements` are now always `undefined`
The optional `latest_orders` field on `getOrders()` and `latest_movements` field on `getUserMovements()` were a Redis hot-cache shadow. With Redis removed from the orders/movements path, the backend no longer populates them — both fields are still declared optional on the response types but will always be `undefined`.
If you were reading them, switch to the paginated `orders` / `movements` arrays:
```typescript theme={null}
// Before — would sometimes return hot-cache entries:
const { latest_orders, orders } = await sdk.trading.getOrders({ page: 1 });
// After — use the paginated array directly:
const { orders } = await sdk.trading.getOrders({ page: 1 });
```
No breaking type change — the fields remain on the response interface as optional.
***
## Open interest
The TS SDK already exposed `open_interest_base` and `open_interest_notional` on `sdk.market.getOpenInterest()` — those fields are now sourced from live margin positions instead of OHLCV candles (where the underlying column was never populated and the values came back null). Existing callers see correct values.
The `OpenInterest` type also gains an optional `open_interest?: string` field as a base-units alias for backward compatibility with consumers that read the older field name.
***
## Upgrade
```bash theme={null}
npm install @0xmonaco/core@0.8.7 @0xmonaco/types@0.8.7 @0xmonaco/react@0.8.7
# or bun
bun add @0xmonaco/core@0.8.7 @0xmonaco/types@0.8.7 @0xmonaco/react@0.8.7
```
# V0.8.8
Source: https://docs.0xmonaco.com/releases/v0.8.8
# Monaco Protocol SDK v0.8.8
## Breaking: authentication moves to ed25519 session-key signing
Authentication no longer uses JWT access/refresh tokens. The wallet now signs once to authorize a locally-generated ed25519 **session key**, and every request is signed with that key. **All existing sessions are invalidated on deploy — every user must reconnect their wallet and sign once.** Backend and frontend must deploy v0.8.8 together; there is no window where both auth schemes work.
What changes for SDK users:
* `AuthState` no longer has `accessToken` / `refreshToken`. It now carries `sessionPublicKey`, `sessionPrivateKey`, `expiresAt`, and `user`. The **session private key is the credential** — persist it (not a token) to restore a session without re-prompting the wallet.
* `sdk.login(clientId)` is unchanged as the entry point and still returns `AuthState`. Request signing is automatic after login (and after `sdk.setAuthState(...)`); there is no token to attach.
* The manual token methods `sdk.auth.refreshToken(...)` and `sdk.auth.revokeToken()` are gone. Use `sdk.refreshAuth()` (or `sdk.auth.refreshSession()`) to extend a session and `sdk.logout()` (or `sdk.auth.revokeSession()`) to revoke it.
* React: `useAuth` exposes `refreshSession` / `revokeSession` (the old `refreshToken` / `revokeToken` are removed). The `useTokenLifecycle` hook and the provider `tokenLifecycle` prop now persist and refresh the **session** (keypair + expiry); their names and config are unchanged.
For direct REST users:
* `POST /api/v1/auth/challenge` and `POST /api/v1/auth/verify` now require a `session_public_key` (64-char lowercase-hex ed25519 public key). `verify` no longer returns any tokens — only `expires_at` and `user`.
* Every authenticated request must be signed with three headers: `X-Monaco-PublicKey`, `X-Monaco-Timestamp` (Unix milliseconds, within 30 seconds of server time), and `X-Monaco-Signature`. The signature covers `METHOD\npath?query\ntimestamp_ms\nSHA256_hex(body)`.
* `POST /api/v1/auth/refresh` and `POST /api/v1/auth/revoke` now take an empty body and are authenticated with those signing headers.
* Backend services authenticate by sending their `sk_` secret key in the `x-server-key` header on every request — there is no token exchange.
See [TypeScript authentication](/sdk/typescript/auth) and [Authenticating Requests](/api-reference#authenticating-requests).
## Added: custom API and WebSocket URLs
`SDKConfig` gains optional `apiUrl` and `wsUrl` overrides. When set, the core SDK (`createMonacoSDK` / `new MonacoSDK`) connects to the given URLs instead of the `network` preset's defaults — useful for self-hosted deployments or CI/e2e stacks reachable only by a custom host (e.g. `http://api-gateway:8080`, `ws://ws-api:8080/ws`). `network` still selects the chain. Both must be valid absolute URLs or the constructor throws `InvalidConfigError`.
```typescript theme={null}
const sdk = new MonacoSDK({
walletClient,
network: "staging",
seiRpcUrl: "https://evm-rpc-testnet.sei-apis.com",
apiUrl: "http://api-gateway:8080",
wsUrl: "ws://ws-api:8080/ws",
});
```
## Upgrade
```bash theme={null}
npm install @0xmonaco/core@0.8.8 @0xmonaco/types@0.8.8 @0xmonaco/react@0.8.8
# or bun
bun add @0xmonaco/core@0.8.8 @0xmonaco/types@0.8.8 @0xmonaco/react@0.8.8
```
# V1.0.16
Source: https://docs.0xmonaco.com/releases/v1.0.16
# Monaco Protocol SDK v1.0.16
This release adds exact tier-resolved fee rates to fee simulation and deprecates the integer/flat fee-bps fields, adds an optional Cloudflare Turnstile token to the faucet, and corrects the HTTP status returned when the matching engine rejects an order-management request. It rolls up the consumer-facing changes shipped since v1.0.7.
## Added
### Exact tier fee rates on fee simulation
`fees.simulateFees` — `GET /api/v1/fees/simulate` and gRPC `FeesService.SimulateFees` — now returns two decimal basis-points strings alongside the existing fields:
| Field | Type | Meaning |
| - | - | - |
| `monacoTakerFeeBpsExact` | `string \| null` | Tier-resolved taker rate the ledger charges this caller, e.g. `"6.5"`. Equals `monacoTakerFee / notional` |
| `monacoMakerRebateBpsExact` | `string \| null` | Tier-resolved maker rebate (negative is a rebate), e.g. `"-1"` |
Fee tiers are per-user and fractional. An integer field cannot represent a rate like `6.5`, so these string fields carry the exact rate the ledger applies. Prefer them for any fee display or reconciliation.
```typescript theme={null}
const fees = await sdk.fees.simulateFees({
tradingPairId: pair.id,
side: "BUY",
price: "65000",
quantity: "0.1",
});
// exact tier-resolved rate the ledger charges this caller
console.log(fees.monacoTakerFeeBpsExact); // e.g. "6.5"
```
### Optional faucet captcha token
`sdk.faucet.mint()` accepts an optional `turnstileToken` (Cloudflare Turnstile). It maps to the REST request-body field `turnstileToken` and gRPC `MintTokensRequest.turnstile_token`. Omitting it keeps today's behavior; a valid token becomes required once the gateway enables faucet captcha enforcement.
```typescript theme={null}
await sdk.faucet.mint({ turnstileToken: "0x..." });
```
## Changed
### Order-management endpoints return the correct HTTP status on rejection
Place, cancel, replace, and batch order endpoints (including conditional orders) now map matching-engine client-fault rejections to their proper HTTP status instead of surfacing them as `500 Internal Server Error`. For example, an invalid TP/SL trigger price now returns `400`.
| gRPC code | HTTP status |
| - | - |
| `invalid_argument`, `out_of_range`, `resource_exhausted` | `400` |
| `not_found` | `404` |
| `failed_precondition`, `already_exists` | `409` |
| `permission_denied` | `403` |
| `unauthenticated` | `401` |
Genuine matching-engine faults still return `500`. Client code that treated every order rejection as a `500` should branch on the specific status.
### Rust REST SDK builds on native-tls
The published `monaco-sdk` crate now builds its `reqwest` HTTP client on native-tls instead of rustls, matching the rest of the Monaco workspace. Consumers on platforms without a system TLS library — for example static musl builds — now need OpenSSL available at build time.
## Deprecated
### Integer and flat fee-bps fields
The following fields are deprecated because they echo the pre-tiered, pair-level rate rather than the caller's per-user tiered fee:
* `monacoTakerFeeBps` / `monacoMakerRebateBps` (integer) on the fee-simulation response
* `makerFeeBps` / `takerFeeBps` (flat) on the trading-pair and account responses
Use `monacoTakerFeeBpsExact` / `monacoMakerRebateBpsExact`, or `GET /api/v1/fees/tier` / `fees.getMyFeeTier`, for the caller's real rate. The deprecated fields remain populated for backward compatibility.
## Upgrade
```bash theme={null}
npm install @0xmonaco/core@1.0.16 @0xmonaco/types@1.0.16 @0xmonaco/react@1.0.16
# or bun
bun add @0xmonaco/core@1.0.16 @0xmonaco/types@1.0.16 @0xmonaco/react@1.0.16
```
No response fields were removed and no endpoint paths or authentication requirements changed. If you read `monacoTakerFeeBps` / `monacoMakerRebateBps` or the flat `makerFeeBps` / `takerFeeBps`, migrate to the exact fields (or `getMyFeeTier`) to display the tier-resolved rate. If you handle order-rejection errors by HTTP status, branch on the specific `4xx` codes above instead of assuming `500`.
# V1.0.2
Source: https://docs.0xmonaco.com/releases/v1.0.2
# Monaco Protocol SDK v1.0.2
This release ships PitPass TraderCodes — Monaco's referral program — alongside the full tiered-fee system. Every user gets a unique referral code; rewards accrue from referred users' trading activity up to three levels deep, funded from net protocol fees and instantly transferable into any trading balance.
## Added
### PitPass TraderCodes
Every Monaco wallet now has a unique TraderCode. Share it to earn a share of the net protocol fee generated by referred users' trades — up to three levels deep.
**How rewards work:**
| Level | Relationship | Rate |
| - | - | - |
| L1 | Direct referral's trades | 30% of net fee |
| L2 | L1's referral's trades | 15% of net fee |
| L3 | L2's referral's trades | 5% of net fee |
Rewards are computed on the realized net fee at fill time (taker fee minus maker rebate) and credited instantly to a per-user rewards bucket. The bucket is app-agnostic — rewards accumulate regardless of which Monaco application generated the trade.
**New endpoints:**
* **REST / TypeScript SDK / gRPC** — `GET /api/v1/pitpass/codes/me` — return the authenticated user's TraderCode
* **REST / TypeScript SDK / gRPC** — `GET /api/v1/pitpass/codes/{code}` — public lookup to validate a referral code at signup
* **REST / TypeScript SDK / gRPC** — `GET /api/v1/pitpass/rewards/balance` — read accrued rewards across all reward tokens
* **REST / TypeScript SDK / gRPC** — `POST /api/v1/pitpass/rewards/transfer` — move rewards into a trading balance (ledger-only, no gas, immediately tradeable)
**Auth verify — new optional field:**
`POST /api/v1/auth/verify` now accepts an optional `referralCode` field. When a new user provides a valid code on first sign-in, a referral relationship is recorded atomically. Invalid codes and codes supplied for returning users are silently ignored and never block sign-in.
```typescript theme={null}
// Capture ?ref=CODE from URL and pass at auth time
await sdk.auth.authenticate("your-client-id", capturedCode);
// Get and share your own code
const { code } = await sdk.pitpass.getMyTraderCode();
// Check earned rewards
const { balances } = await sdk.pitpass.getRewardsBalance();
// Move rewards to trading balance
await sdk.pitpass.transferRewards({ token: "0xusdc...", amount: "1000000" });
```
### Tiered fees
Fee tiers are now applied per-user based on a 14-day rolling weighted volume (`14d perp + 2.5 × 14d spot`). Six tiers are available. Volumes are recomputed daily at 00:00 UTC. The current tier and the volume needed to reach the next tier are returned by `GET /api/v1/fees/tier` (added in v0.8.18).
## Upgrade
```bash theme={null}
npm install @0xmonaco/core@1.0.1 @0xmonaco/types@1.0.1 @0xmonaco/react@1.0.1
# or bun
bun add @0xmonaco/core@1.0.1 @0xmonaco/types@1.0.1 @0xmonaco/react@1.0.1
```
The new `pitpass` namespace is additive. Auth verify gains one optional field (`referralCode`). All other existing endpoint shapes, field names, and authentication requirements are unchanged.
# V1.0.21
Source: https://docs.0xmonaco.com/releases/v1.0.21
# Monaco Protocol SDK v1.0.21
This release adds an authoritative portfolio-analytics API (`sdk.portfolio`), removes the standalone backend-authentication endpoint in favor of the `x-server-key` header, and corrects the fee totals behind the portfolio chart and stats. It rolls up the consumer-facing changes shipped since v1.0.16.
## Added
### Portfolio analytics API
A new `sdk.portfolio` namespace exposes five authenticated, application-scoped portfolio queries backed by `GET /api/v1/accounts/me/portfolio/*` (gRPC `AccountsService.GetPortfolio*`). Calculations run in Core, so every caller sees the same authoritative numbers.
| Method | Endpoint | Returns |
| - | - | - |
| `getSummary({ period })` | `GET .../portfolio/summary` | Current equity split plus period activity (volume, trade/order counts, fees paid) |
| `getPerformance({ period })` | `GET .../portfolio/performance` | Transfer-neutral dollar PnL, sampled time-weighted return, max drawdown, and an equity series |
| `getRealizedPnl({ period })` | `GET .../portfolio/realized-pnl` | Realized PnL split into perp closes, spot trades, funding, and fees, with closed-trade stats and a bucketed series |
| `getAssets({ period })` | `GET .../portfolio/assets` | Per-asset balances priced by asset ID, with allocation percentages and total spot equity |
| `getMargin()` | `GET .../portfolio/margin` | Current parent-account margin totals and per-risk-bucket projections |
The four historical methods accept an optional `period` — `"1d"`, `"1w"`, `"1m"`, or `"all"` — and default to `all` when omitted. `getMargin()` is current-only and takes no period.
Every response carries an explicit query cutoff and retained-history coverage, so partial windows are surfaced rather than silently truncated. Decimal amounts and percentages are strings. Dependent values that cannot be computed — a return for a window with no starting equity, or a value for an asset with no spot price — are omitted rather than reported as `0`.
```typescript theme={null}
const summary = await sdk.portfolio.getSummary({ period: "1m" });
console.log(summary.current.totalEquity, summary.activity.feesPaid);
const perf = await sdk.portfolio.getPerformance({ period: "1w" });
console.log(perf.returnPct, perf.maxDrawdown?.pct);
const margin = await sdk.portfolio.getMargin();
console.log(margin.total.parentAvailableCollateral, margin.riskBuckets.length);
```
See the [Portfolio Analytics](/sdk/typescript/portfolio) SDK guide for the full response shapes.
## Removed
### Standalone backend authentication (breaking)
The dedicated backend-auth exchange is gone from the wire contract:
* REST `POST /api/v1/auth/backend` (operation `authenticate_backend`) is removed
* gRPC `AuthService.BackendAuth` and its generated client method are removed
* The `BackendAuthRequest { secret_key }` and `BackendAuthResponse { app_id, client_id, name }` messages are removed from the protobuf contract and the generated OpenAPI schema
Backend applications already authenticate every request with their `sk_` secret key in the `x-server-key` header (gRPC metadata); nothing there changes. The TypeScript SDK's `authenticateBackend()` helper was removed back in v0.8.10 — set the key once with `sdk.setServerKey("sk_live_...")`. Only integrations that still call the raw `POST /api/v1/auth/backend` endpoint or the `BackendAuth` RPC need to migrate: drop the exchange and send `x-server-key` directly.
## Fixed
### Portfolio stats fee totals
`getPortfolioStats` (`GET /api/v1/accounts/me/portfolio`) now sources `pnlBreakdown.fees` and `totalPnl` from the authoritative trade-ledger fee total — the same source as `feesPaid` — instead of the PnL service's cumulative fee counter. That counter under-recorded margin fills whose order carried no margin position id, which understated fees and overstated `totalPnl`. Because stats read the ledger directly, the correction applies across all history, not just data recorded after this release. The realized headline `pnl` field was already correct and is unchanged.
### Portfolio chart fee metrics
`getPortfolioChart` reads persisted per-account fee snapshots rather than the trade ledger, and those snapshots are now recorded correctly. Two fixes to the PnL service:
* every margin fill's fee is attributed to its owning margin account, including closing and reducing fills whose order carries no position id (previously dropped), and
* margin taker fees now include the `application_taker_fee` component, matching the spot path and `feesPaid`.
This corrects the chart's `fees`, `realizedPnl`, and `totalPnl` series metrics for data recorded after the fix; earlier buckets are not retroactively restated.
### SDK sends an explicit User-Agent header
`@0xmonaco/core` now sets a `User-Agent` header (`@0xmonaco/core`) on every REST request. Monaco's edge rejects requests with an empty or absent `User-Agent`, so SDK traffic could intermittently be blocked. In the browser the runtime controls `User-Agent` and this value is ignored, since it is a forbidden header there.
## Upgrade
```bash theme={null}
npm install @0xmonaco/core@1.0.21 @0xmonaco/types@1.0.21 @0xmonaco/react@1.0.21
# or bun
bun add @0xmonaco/core@1.0.21 @0xmonaco/types@1.0.21 @0xmonaco/react@1.0.21
```
`sdk.portfolio` is additive — no existing method or response field changed shape. If any integration still calls `POST /api/v1/auth/backend` or the gRPC `BackendAuth` RPC, remove that call and send the `sk_` secret key in the `x-server-key` header on each backend request instead. If you reconcile against portfolio stats `fees` / `totalPnl`, expect corrected values across all history (higher fees, lower `totalPnl`); the chart's `fees` / `realizedPnl` / `totalPnl` series are corrected only for data recorded after this release.
# V1.0.24
Source: https://docs.0xmonaco.com/releases/v1.0.24
# Monaco Protocol SDK v1.0.24
This release corrects three isolated-margin collateral behaviors. The changes are server-side and apply to every client — TypeScript, Rust REST, and Rust gRPC — so no SDK upgrade is required to receive them. One new validation error is worth handling if you reduce position margin. It rolls up the consumer-facing changes shipped since v1.0.21 (v1.0.22 and v1.0.23 were backend-only).
## Fixed
### Isolated-margin partial-fill collateral returns to the parent account
Cancelling (or IOC-releasing) the unfilled remainder of a partially filled isolated-margin limit order now returns the excess auto-funded risk-bucket collateral to the parent margin account's free collateral immediately. Previously the full order-sized allocation stayed locked in the bucket until the position closed, which inflated bucket equity and deferred liquidation beyond what the isolated margin implied.
### `reducePositionMargin` rejects below-maintenance reductions
`reducePositionMargin` (`POST /api/v1/positions/{positionId}/margin/reduce`; gRPC `PositionsService.ReducePositionMargin`) now returns `400` / `INVALID_ARGUMENT` when a reduction would take the position's margin below its maintenance requirement. The error includes the maintenance floor, the resulting post-reduction margin, and the maximum amount you can remove. Previously margin could be reduced all the way to zero, after which the engine silently re-margined the position at the pair's maximum leverage.
If you call reduce-margin, add handling for this rejection — it is the only client-visible action item in this release.
### `newIsolatedMargin` reports post-transfer position margin
The `newIsolatedMargin` field in the add/reduce position-margin responses now reflects the position's margin after the transfer, instead of the risk bucket's allocated principal (which could include unrelated bucket equity).
## Upgrade
These fixes are live server-side and apply regardless of SDK version. The packages are published at `1.0.24` for version alignment; the TypeScript SDK code is unchanged from v1.0.21, so upgrading is optional:
```bash theme={null}
npm install @0xmonaco/core@1.0.24 @0xmonaco/types@1.0.24 @0xmonaco/react@1.0.24
# or bun
bun add @0xmonaco/core@1.0.24 @0xmonaco/types@1.0.24 @0xmonaco/react@1.0.24
```
No new endpoints, fields, or methods were added, and no request or response shapes changed.
# V1.0.26
Source: https://docs.0xmonaco.com/releases/v1.0.26
# Monaco Protocol SDK v1.0.26
This release introduces one-way futures order semantics, faucet quota introspection, a mandatory market-order price band, and a hardened WebSocket reconnect contract. It rolls up the consumer-facing changes since v1.0.24 — v1.0.25 was a backend-only deploy — and adds new SDK surface plus observable behavior changes on every client (TypeScript, Rust REST, and Rust gRPC), so upgrading is recommended and there are a few action items worth reviewing before you deploy.
## Added
### Faucet quota introspection
New `sdk.faucet.getInfo()` (`GET /api/v1/faucet/info`, session-authenticated) returns the authenticated wallet's faucet quota and recent history: `remainingRequests24h`, `dailyLimit`, an optional `nextAvailableAt` (ISO 8601 — when the wallet's next faucet slot frees under the current daily limit; omitted, never null, when the rolling 24h window is empty or the limit is 0), and up to five `recentRequests` newest-first. Quota is wallet-scoped across apps and uses the same rolling-window accounting as `mint()` enforcement. Call it before `request_faucet`/`mint()` to show remaining quota instead of provoking a `429`. A matching `get_faucet_info` MCP tool ships for agents (non-mainnet networks only).
### Position TP/SL: full-close legs and OCO
`sdk.positions.attachPositionTpSl` (`POST /api/v1/positions/{positionId}/tp-sl`) adds a per-leg `closePosition` flag that resolves the full live position size when the leg triggers (omit `quantity` when `closePosition: true`), and a request-level `oco` flag that cancels the sibling leg after one triggers (`oco: true` requires both a take-profit and a stop-loss leg).
### Order cancellation cause
Order responses and WebSocket order events now expose a nullable, stable `terminalReason` when an order is cancelled: `USER_REQUESTED`, `REPLACED`, `LIQUIDATION`, `INSUFFICIENT_MARGIN`, `REDUCE_ONLY_EXHAUSTED`, `POSITION_CLOSED`, `OCO_SIBLING_TRIGGERED`, or `SYSTEM`.
### Perp risk tiers: base-quantity position cap
Perp risk tiers add `maxPositionSize`, the authoritative maximum absolute base-asset quantity for the tier. `maxPositionNotional` remains as a deprecated compatibility alias (it always carried a base quantity, not a notional) — read `maxPositionSize`.
### SDK configuration and WebSocket options
* `createMonacoSDK` accepts an optional `pollingInterval` (milliseconds) that tunes the internal viem public client's transaction-receipt poll frequency. Must be a positive finite number, otherwise `createMonacoSDK` throws `InvalidConfigError`.
* `createMonacoWebSocket(baseUrl, options)` accepts `maxReconnectAttempts`, `reconnectBaseDelayMs`, and `connectionTimeoutMs`, plus a new `onResync` callback (see Changed).
## Changed
### One-way futures order semantics
Authenticated order creation and margin risk simulation now use one-way futures semantics:
* **`side` is the authoritative direction.** `positionSide` is optional and deprecated; when supplied it must agree with `side` (`BUY`↔`LONG`, `SELL`↔`SHORT`).
* **Leverage is required for normal margin orders** and optional for reduce-only orders.
* **Entry-attached TP/SL is rejected.** Passing `takeProfit`/`stopLoss` on `placeLimitOrder`/`placeMarketOrder` now returns `400` — create position TP/SL with `attachPositionTpSl` after the position opens.
### Market-order price protection
Market orders now execute inside a mandatory server-side protective price band of 500 bps around a robust reference price (the mark price, or the median of best bid, best ask, and last trade). Fills beyond the band are cut off: the portion that fills inside the band settles and any remainder is cancelled; if nothing can fill inside the band the order is rejected as insufficient liquidity; and a market order on a market with no usable reference price is rejected as unavailable. A client `slippageToleranceBps` can only *tighten* the band, never widen it. The order-risk simulation endpoints reflect the same band and can return the same rejections. No wire types changed.
### WebSocket reconnect contract
Auto-reconnect now retries indefinitely by default with full-jitter exponential backoff capped at 30s (previously it gave up after 5 attempts) — pass `maxReconnectAttempts` to restore a finite cap. `onStatusChange` now also reports `"reconnecting"` while backing off, the client answers a server-initiated `{"type":"Ping"}` with a `Pong`, and a new `onResync` callback fires after every automatic reconnect once the connection is re-authenticated and re-subscribed. `onResync` receives `slowClient: true` when the server shed the connection with close code `1013`, so apps can refetch REST snapshots and reconcile by `(id, updatedAt)`.
**Correction since this release.** The original wording is left in place above; the correction is:
* *"reconcile by `(id, updatedAt)`"* — for orders that key cannot converge. Order detail reports `updatedAt` from the matching engine's event clock while order lists report it from the database commit clock, so the two endpoints report different values for the same order, and the commit clock can move without any state change. Orders now carry an opaque, compare-only `version` on the REST responses, on every order WebSocket event, and on the `orders` snapshot frame: reconcile on `(id, version)`, taking a snapshot row when `snapshot.version >= local.version`, and falling back to a full field comparison when it is absent (an order last written before the field existed). See [Order state versions](/developers/trade#order-state-versions).
### Faster transaction-receipt polling
The SDK's internal viem public client now polls for receipts every 750ms by default (previously viem's 4000ms), so `sdk.vault.deposit` and `sdk.waitForTransaction` resolve within about one to two Sei blocks. Set `pollingInterval: 4000` to restore the previous cadence.
### Faucet daily-limit counting
The faucet daily limit now counts every mint call in the rolling 24h window rather than distinct clock minutes, closing a bypass where multiple calls in the same minute consumed a single quota slot. `getInfo()`'s `remainingRequests24h` and `nextAvailableAt` reflect the same per-call counting; the minute truncation on `recentRequests[].requestedAt` is display grouping only.
### Development WebSocket ingress
The `development` network preset now connects WebSockets to `wss://ws-develop.apimonaco.xyz/ws`. The previous `wss://develop.apimonaco.xyz/ws` path remains supported during the migration window, so older SDK versions and custom `wsUrl` overrides keep working unchanged.
## Fixed
### Batch-replace taker fees attributed to the owning account
The PnL service now attributes the taker fee on a batch-replace fill to its owning margin account. A batch replace produces multiple result orders and carries no top-level taker order, so those taker fees were previously dropped — understating persisted per-account fee totals, most visibly for market-making accounts that reprice via batch replace. This corrects the cumulative `fees`, `realizedPnl`, and `totalPnl` behind `getPortfolioChart` for data recorded after this change.
## Security
* **Session public-key reuse returns `409`.** Re-running verify or creating a delegated-agent session with an already-registered session public key now returns `409 Conflict` ("resource already exists") / gRPC `already_exists`, instead of a `500` that leaked the Postgres detail and unique-constraint name. Generate a fresh session keypair per session.
* **Hardened error envelopes.** Other database failures return a generic `500` "Database error" and a malformed JSON body returns `400` "Invalid JSON payload" — none echo engine detail, constraint names, query text, or the raw parser message. Successful requests are unchanged.
* **Withdrawal-index validation.** Public `GET /api/v1/withdrawals/{withdrawal_index}` now rejects a negative or non-integer index with `400 Bad Request` instead of forwarding it to a lookup that returned a misleading `404`. Valid non-negative lookups are unchanged — `404` still means the index was never allocated.
## Upgrade
```bash theme={null}
npm install @0xmonaco/core@1.0.26 @0xmonaco/types@1.0.26 @0xmonaco/react@1.0.26
# or bun
bun add @0xmonaco/core@1.0.26 @0xmonaco/types@1.0.26 @0xmonaco/react@1.0.26
```
Action items to review:
1. **Move entry-attached TP/SL to `attachPositionTpSl`.** Any `placeLimitOrder`/`placeMarketOrder` call that passes `takeProfit`/`stopLoss` now returns `400`. Open the position first, then attach TP/SL — optionally with the new `closePosition` and `oco` flags.
2. **Drop `positionSide`; supply `leverage`.** Rely on `side` for direction, and pass `leverage` on normal margin orders (omit it only for reduce-only orders).
3. **Handle new market-order outcomes.** A market order can now come back partially filled with the remainder cancelled, or be rejected as insufficient liquidity or market unavailable. `slippageToleranceBps` only tightens the 500 bps band.
4. **Generate a fresh session keypair per session** to avoid the new `409` on reuse.
5. **Reconsider WebSocket reconnect handling.** Auto-reconnect now retries indefinitely by default — set `maxReconnectAttempts` for a finite cap, and use `onResync` to refetch snapshots and reconcile after a reconnect.
# V1.0.29
Source: https://docs.0xmonaco.com/releases/v1.0.29
# Monaco Protocol SDK v1.0.29
This release adds a public market pulse feed, an all-markets funding-history endpoint, and cursor pagination for full order and trade history. It also widens the market-order price band, re-enables entry-attached TP/SL, and makes several portfolio and position figures more accurate. It rolls up the consumer-facing changes since v1.0.26 across every client (TypeScript, Rust REST, and Rust gRPC), so upgrading is recommended and there are a few action items worth reviewing before you deploy.
## Added
### Market pulse feed
New `sdk.pulse.getPulse()` (`GET /api/v1/pulse`, public — no auth) returns a cached market news and sentiment feed: `generatedAt` (ISO 8601 UTC, also the response ETag), `watchlist`, and `items[]` — each an asset ticker (or `"MARKET"` for a cross-asset story) with a `sentiment` of `"Bullish"`, `"Neutral"`, or `"Bearish"`, a `summary` (≤240 chars), `updatedAt`, and `sources[]` (`url`, `author`, `source`, `createdAt`, `relevance` 1–5). The snapshot refreshes hourly; responses carry an `ETag` and support `If-None-Match`/`304`, and return `503` until the cache warms. Also on gRPC `PulseService.GetPulse`.
### All-markets funding history
New `sdk.market.listAllFundingHistory(params?)` (`GET /api/v1/market/funding/history`, public) returns settled funding across every perp market in one call, so you no longer fan out one `listFundingHistory` request per pair. Optional `startTime`/`endTime` (ms-epoch strings, inclusive on `settledAt`), `page`, `pageSize` (max 100), and `orderBy` (`"ASC"`/`"DESC"`, default `DESC`); the response is `{ records, total, page, pageSize }` and each `FundingRecord` carries its own `tradingPairId`. Also on gRPC `MarketService.ListAllFundingHistory`.
### Cursor pagination for orders and trades
`sdk.trading.getPaginatedOrders` and `sdk.profile.getUserTrades` (gRPC `ListOrders`, `GetUserTrades`, `ListUserTrades`) accept a `pageToken` cursor and return a `nextPageToken`:
* **Omit `pageToken`** for legacy page-number pagination — the recent hot window only.
* **Pass `pageToken: ""`** (empty string) to start a cursor walk over full history (hot and archived rows merged), newest first — or oldest first with `orderBy: "ASC"`.
* **Pass a previous `nextPageToken`** to resume the walk.
`nextPageToken` is non-empty whenever a page returns any rows; a zero-row page with an empty token means the walk is exhausted. In cursor mode the exact `total`/`page`/`totalPages` counts are omitted (returned as `0`).
### Funding data on records and the screener
* `FundingRecord` gains a `carried` flag — `true` when the window settled at the carried last rate under the sparse-sample rule — and `listFundingHistory` accepts `orderBy` (`"ASC"`/`"DESC"`).
* Screener rows (`sdk.market.getScreener`, `ScreenerItem`) gain `latestFundingRate` (the latest capped settled rate, a decimal fraction per window) and `lastFundingTime` (window close, ms epoch). Both are `null` for spot pairs and for perps with no settled window yet.
### Terminal position lifetime figures
Terminal position rows (`CLOSED`/`LIQUIDATED`/`EXPIRED`) now report lifetime totals instead of the sliver left before the final closing fill: `size` is the total quantity closed over the position's life and `isolatedMargin` is the total initial margin allocated across its reducing executions. Two optional fields are added — `netRealizedPnl` (lifetime realized PnL net of trading fees and funding) and `realizedRoe` (that figure as a percentage of the total allocated initial margin, i.e. leverage-aware realized return). Both are absent, and the stored row values served instead, when a position's reducing executions do not cover its whole lifetime. Open rows are unchanged.
## Changed
### Market-order price band widened to 1,000 bps
The mandatory server-side protective band around the reference price is now **1,000 bps (10%)** — widened from 500 bps — and triggered TP/SL market legs use a wider **1,200 bps (12%)** band. Market orders execute immediately-or-cancel: the quantity available inside the band fills and the remainder is cancelled; a zero-fill order still rejects as insufficient liquidity, and a market on a market with no usable reference price is still rejected as unavailable. A client `slippageToleranceBps` can only *tighten* the band, never widen it. The fee-simulation endpoint's default market slippage buffer follows the band to 1,000 bps, so preflight lock estimates match what placement reserves. No wire types changed.
### Entry-attached TP/SL re-enabled
Passing `takeProfit`/`stopLoss` on `placeLimitOrder`/`placeMarketOrder` is accepted again for margin **entry** orders under one-way semantics — the child direction follows the order side. The legs activate when the order fills and cancel if it is cancelled or expires unfilled. (This reverses the v1.0.26 rejection; spot and reduce-only orders still reject entry-attached TP/SL.)
### Position-based closed-trade win rate
Closed-trade win statistics now classify each **position** (one flat→open→flat round-trip) once by its net realized PnL after all fees, rather than per closing fill. `sdk.portfolio.getRealizedPnl().closedTrades` adds a `positions` total (`winners + losers + breakEven`), and `sdk.profile.getPortfolioStats().winLossRatio` is position-based; `count` stays fill-based.
### Batch orders reject time-in-force on market items
Batch order creation now rejects `timeInForce` on `MARKET` items, matching the single-order endpoint — market orders take no time-in-force and always execute IOC-style inside the protective band. The SDK rejects this client-side before the request is sent.
### Funding-state cadence
`getFundingState().nextFundingTime` now reflects a market's actual settlement cadence, and `estimatedNextFundingRate` refreshes as new samples arrive.
### Staging WebSocket ingress
The `staging` network preset now connects WebSockets to `wss://ws-staging.apimonaco.xyz/ws` (the `development` preset moved to `wss://ws-develop.apimonaco.xyz/ws` in v1.0.26). The previous `wss://staging.apimonaco.xyz/ws` path remains supported during the migration window, so custom `wsUrl` overrides and older SDK versions keep working unchanged.
### Trade settlement status
On trade records (the `settlementStatus` field on `GetUserTrades`/`ListUserTrades` in the REST and gRPC wire), the `SETTLED_ON_CHAIN` value is retired — the valid values are now `PENDING`, `SETTLED` (terminal), and `FAILED`. This is the **trade-level** settlement status only; the order lifecycle `OrderStatus` is unchanged and still includes `SETTLED_ON_CHAIN`.
### Scoped maintenance mode
During a maintenance window the API keeps sign-in, order cancels, position closes, risk simulations, and delegated-session creation available, and rejects mutating operations (new and replace orders, TP/SL, margin changes, collateral transfers, deposits, withdrawals, faucet mint, delegated-agent config, and reward transfers). Rejected requests return REST `503` with a stable `OPERATIONS_BLOCKED` code (gRPC `UNAVAILABLE` with `x-maintenance-blocked` metadata), so a deliberate maintenance block is distinguishable from a generic outage.
## Fixed
### Retryable `503` on matching-engine unavailability
`POST /api/v1/positions/{positionId}/close`, `POST /api/v1/positions/{positionId}/tp-sl`, `POST /api/v1/pitpass/rewards/transfer`, and `GET /api/v1/orderbook/{tradingPairId}` now return `503` (retryable) instead of `500` when the matching engine is momentarily unreachable or restarting. Requests and responses are unchanged; non-transient engine faults still return `500`.
## Upgrade
```bash theme={null}
npm install @0xmonaco/core@1.0.29 @0xmonaco/types@1.0.29 @0xmonaco/react@1.0.29
# or bun
bun add @0xmonaco/core@1.0.29 @0xmonaco/types@1.0.29 @0xmonaco/react@1.0.29
```
Action items to review:
1. **Market band is now 1,000 bps.** A market order can partially fill with the remainder cancelled; `slippageToleranceBps` only tightens the band. Fee-simulation now defaults to a 1,000 bps market buffer.
2. **Trade settlement status dropped `SETTLED_ON_CHAIN`.** If you read the trade-level `settlementStatus` over the wire, handle only `PENDING`, `SETTLED`, and `FAILED` — the order `status` enum is unaffected.
3. **Walk full history with a cursor.** To read beyond the recent window, start with `pageToken: ""` and follow `nextPageToken` until a zero-row page.
4. **Entry-attached TP/SL works again** for margin entries — attach `takeProfit`/`stopLoss` at placement instead of after the position opens if you prefer.
5. **Treat maintenance `503`s as retryable.** A REST `503` with `OPERATIONS_BLOCKED`, and the matching-engine `503`s above, are safe to retry.
# V1.0.3
Source: https://docs.0xmonaco.com/releases/v1.0.3
# Monaco Protocol SDK v1.0.3
This release adds closed position execution history, conditional cross liquidation prices across positions and risk simulations, and fixes vault withdrawal early-return and fee-tier volume unit reporting.
## Added
### Closed position execution history
`positions.listPositionHistory` now accepts an optional `reductionOnly` parameter. When set to `true`, the response includes only reductions, closes, and liquidations — filtering out opens, partial fills, funding, and margin-change events.
Each reduction row now carries immutable per-execution close facts computed by the backend:
| Field | Type | Description |
| - | - | - |
| `entryPrice` | string | Position's average entry price at the time of close |
| `allocatedInitialMargin` | string | Initial margin committed to the position when it was opened |
| `fundingPaid` | string | Cumulative funding charges paid over the position's life |
| `netRealizedPnl` | string | Realized PnL net of fees and funding at close |
| `realizedRoe` | string | Return on equity (net PnL ÷ allocated initial margin) |
| `positionSide` | string | `"LONG"` or `"SHORT"` |
Use `executionPrice` (the fill price at close) rather than `markPrice` when calculating realized returns. The backend computes `netRealizedPnl` and `realizedRoe` for you — no mark-price approximation needed.
```typescript theme={null}
const history = await sdk.positions.listPositionHistory({
positionId,
reductionOnly: true,
});
for (const event of history.events) {
console.log(`Closed at ${event.price}, net PnL: ${event.netRealizedPnl}, ROE: ${event.realizedRoe}`);
}
```
## Changed
### Conditional cross liquidation prices
Positions and risk simulations now expose liquidation price estimates for cross-margin positions.
**Positions** — `listPositions`, `getPosition`, and `getPositionRisk` return `liquidationPrice` for all positions including cross. For cross positions the value is **conditional**: only the target position's mark moves while all other marks in the same cross risk bucket are held fixed. Other position marks, funding, realized PnL, fees/reserves, and collateral can all change the actual liquidation threshold. An absent or blank value means unavailable — never zero.
**Risk simulations** — `simulateOrderRisk`, `simulateParentMarginOrderRisk`, and `simulateRiskBucketOrderRisk` now return:
* `freeCollateralAfter`: string — projected free collateral after the simulated fill
* `estimatedLiquidationPrice?`: string — conditional per-position estimate for the target position; same conditional semantics as above
Cross simulations with `marginMode: "CROSS"` require `selectedTradingPairIds`. All three simulation endpoints accept an optional `reduceOnly` boolean that restricts the simulation to shrinking an existing exposure.
```typescript theme={null}
const sim = await sdk.positions.simulateRiskBucketOrderRisk({
marginMode: "CROSS",
selectedTradingPairIds: [tradingPairId],
side: "LONG",
positionSide: "LONG",
orderType: "LIMIT",
price: "50000",
quantity: "0.1",
leverage: "10",
});
console.log(`Free collateral after: ${sim.freeCollateralAfter}`);
if (sim.estimatedLiquidationPrice) {
console.log(`Conditional liq price: ${sim.estimatedLiquidationPrice}`);
}
```
## Fixed
### `vault.withdraw` with `autoWait: false` returned too late
`vault.withdraw(assetId, amount, false)` previously blocked for up to 5 minutes polling for the merkle proof before returning. The proof is never ready at initiation time — so the wallet signing prompt was never shown and the balance appeared stuck.
`vault.withdraw(..., false)` now returns immediately after the withdrawal is registered:
```typescript theme={null}
const { withdrawalIndex, status } = await sdk.vault.withdraw(assetId, amount, false);
// status === 'awaiting_proof'
// Later, once the proof is confirmed (poll or webhook):
await sdk.vault.retryWithdrawal(withdrawalIndex);
```
Use `retryWithdrawal(withdrawalIndex)` to complete the on-chain submission once the merkle proof is available. The `withdrawalIndex` is stable across retries.
### `fees.getMyFeeTier` volume fields now in whole USD
`getMyFeeTier` previously returned `weightedVolume14d`, `spotVolume14d`, and `perpVolume14d` in raw on-chain micro-USDC units (10⁶× too large), which caused every user to resolve to a higher fee tier than their actual 14-day volume warranted.
All three fields now return whole-USD values — e.g. `"5000000"` means $5 M, not $5. Fee tier resolution is now correct.
## Upgrade
```bash theme={null}
npm install @0xmonaco/core@1.0.3 @0xmonaco/types@1.0.3 @0xmonaco/react@1.0.3
# or bun
bun add @0xmonaco/core@1.0.3 @0xmonaco/types@1.0.3 @0xmonaco/react@1.0.3
```
All changes are additive or fixes — no existing field names, endpoint paths, or authentication requirements changed.
# V1.0.32
Source: https://docs.0xmonaco.com/releases/v1.0.32
# Monaco Protocol SDK v1.0.32
This release makes cursor pagination the default for order and trade history and adds full-history iterators, completes the position and funding types, fixes two portfolio and margin accuracy issues, and points the mainnet WebSocket preset at its dedicated ingress. The changes span the TypeScript, Rust REST, and Rust gRPC clients. Upgrading is recommended; the pagination default is the one action item worth reviewing before you deploy.
## Added
### Full-history iterators for orders and trades
New `sdk.trading.iterateOrders(params?)` and `sdk.profile.iterateUserTrades(params?)` async generators walk the entire history (hot and archived rows merged) for you: they take the existing filters plus `pageSize`, manage the `pageToken`/`nextPageToken` cursor internally, and yield one order or trade at a time until the walk is exhausted — so you no longer hand-roll the cursor loop.
### Multi-status order filter
`sdk.trading.getPaginatedOrders` accepts an array of statuses for its `status` filter (`OrderStatusFilter`, for example `["SUBMITTED", "PARTIALLY_FILLED"]`), sent as a single comma-separated request instead of one call per status. A single status string keeps working unchanged.
### Completed position and funding types
`PositionStatus` now includes the terminal statuses `LIQUIDATED` and `EXPIRED` (previously `OPEN | CLOSED | LIQUIDATING`), and a new `PositionStatusFilter` narrows the list-positions `status` filter to the values the endpoint accepts (`OPEN`/`CLOSED`/`LIQUIDATING`/`LIQUIDATED`; an `EXPIRED` position matches under a `CLOSED` filter). `FundingRecord` now types the per-window fields the wire already returned — `epoch`, `fundingDeltaPerUnit`, `referencePrice`, `sampleCount`, `windowStartedAt`, `windowClosedAt`, and `settledAt` (the sort key for `listAllFundingHistory`). No wire behavior changed; these values are now readable type-safely.
## Changed
### Cursor pagination is now the default for orders and trades
`sdk.trading.getPaginatedOrders` (`GET /api/v1/orders`) and `sdk.profile.getUserTrades` (`GET /api/v1/accounts/trades`) are cursor-paginated by default. When you pass neither `page` nor `pageToken`, the SDK now starts a cursor walk (sending `pageToken: ""`) over full history — hot and archived rows merged, newest first — instead of returning a legacy page-number window. Legacy page-number (offset) pagination is **deprecated** but still available:
* Pass the deprecated `page` to opt back into offset mode — it reads the recent hot window only and populates the legacy counts.
* The response `total`, `page`, and `totalPages` fields are deprecated and return `0` in cursor mode (which has no exact total) — drive "load more" UIs on `nextPageToken`, not "page N of M".
* `pageSize` now allows up to `1000` in cursor mode; the `100` cap applies only to legacy page-number mode.
The same deprecations are annotated on the Rust REST and gRPC clients (`page` on `ListOrdersRequest`/`GetUserTradesRequest`/`ListUserTradesRequest`, and `total`/`page`/`total_pages` on their responses) and in the generated OpenAPI spec.
### React hooks and MCP tools follow the cursor default
`useUserOrders` and `useProfile` now fetch with cursor pagination internally; they read the newest rows only, so returned data is unchanged, but the underlying responses no longer carry exact totals (`total`/`page`/`totalPages` are `0`). On the MCP server, `get_orders` gains an optional `pageToken` cursor (with `page` deprecated).
### Mainnet WebSocket ingress
The `mainnet` network preset now connects WebSockets to the dedicated ingress `wss://ws.apimonaco.xyz/ws`. The previous `wss://api.monaco.xyz/ws` host never resolved, so this is not a behavior change for any working client. Custom `wsUrl` overrides are unaffected.
## Fixed
### Spot equity marks at the live trade print
Spot holdings in the portfolio endpoints (`GET /api/v1/portfolio/stats` and `GET /api/v1/portfolio/chart` — `spotEquity`, `totalEquity`, and the equity series) are now marked at the last trade print on the asset's preferred spot pair, matching how the streaming PnL feed already valued them. They were previously marked at the latest one-minute candle close, which lagged the live print. Spot equity values shift accordingly, and an asset whose only trades have aged into the trade archive is now priced instead of being valued at zero. Perpetual positions were unaffected. No request or response fields changed.
### First-use cross risk-bucket order preview
`POST /api/v1/margin/risk-buckets/simulate-order-risk` (and gRPC `SimulateRiskBucketOrderRisk`) no longer returns `400`/`INVALID_ARGUMENT` for a first-use `CROSS` risk bucket. The preview now snapshots the not-yet-created deterministic cross risk bucket virtually — funded from the parent margin account's unallocated collateral, matching live order placement — including after the previous cross risk bucket was fully closed. No request or response fields changed.
## Upgrade
```bash theme={null}
npm install @0xmonaco/core@1.0.32 @0xmonaco/types@1.0.32 @0xmonaco/react@1.0.32
# or bun
bun add @0xmonaco/core@1.0.32 @0xmonaco/types@1.0.32 @0xmonaco/react@1.0.32
```
Action items to review:
1. **Order and trade history is cursor-paginated by default.** Omitting `page`/`pageToken` now returns a cursor walk over full history, not a recent page-number window, and `total`/`page`/`totalPages` are `0` in cursor mode. Move "page N of M" UIs onto `nextPageToken`, or pass the deprecated `page` to keep the old behavior for now.
2. **Use the iterators for full history.** `trading.iterateOrders()` and `profile.iterateUserTrades()` manage the cursor for you.
3. **Larger cursor pages.** `pageSize` can go up to `1000` in cursor mode.
4. **Spot equity values shift.** Portfolio `spotEquity`/`totalEquity` now track the live spot print; expect small differences from the previous candle-close marking.
5. **Mainnet WebSocket host.** If you hard-coded `wss://api.monaco.xyz/ws`, switch to the `mainnet` preset (or `wss://ws.apimonaco.xyz/ws`).
# V1.0.33
Source: https://docs.0xmonaco.com/releases/v1.0.33
# Monaco Protocol SDK v1.0.33
This release scopes the MCP server behind capability profiles — a breaking change for authenticated MCP sessions — adds 36 MCP tools spanning perps, positions, portfolio, margin accounts, and related account/market data, and makes batch order replacement cancel-first across the TypeScript, Rust REST, and Rust gRPC clients. It also fixes `Retry-After` parsing on `APIError` and the `request_faucet` MCP tool, and corrects stale JWT references in client JSDoc. The MCP capability default is the one action item worth reviewing before you upgrade.
## Breaking: authenticated MCP sessions are scoped by a capability profile
Authenticated MCP sessions are now bounded by `MONACO_CAPABILITY` (`read` | `market-maker` | `full`, default `read`). Supplying a private key no longer unlocks the entire authenticated tool set: by default a key grants read-only tools, order placement/cancel/replace require `market-maker`, and fund-movement or privilege tools (vault approve/deposit/withdraw and `transfer_rewards`) require `full` or an explicit `MONACO_TOOL_ALLOWLIST` entry.
`MONACO_NETWORK` is now **required** whenever a private key is configured — the server no longer silently defaults an authenticated session to `mainnet`. New `MONACO_TOOL_ALLOWLIST` and `MONACO_TOOL_DENYLIST` variables give per-tool overrides, with the denylist winning over both the profile and the allowlist. See [Capability profiles](/sdk/mcp-server#capability-profiles) for the full policy.
## Added
### 36 new MCP tools
The MCP server adds 36 tools covering perps, positions, portfolio, margin accounts, and related account/market data that were previously absent from the MCP surface:
* **Perp market data** — `get_perp_market_config`, `get_perp_market_summary`, `get_mark_price`, `get_index_price`, `get_funding_state`, `get_open_interest`, `get_funding_history`, `get_all_funding_history`
* **Market** — `get_screener`, `get_market_stats`
* **Positions** — `get_positions`, `get_position`, `get_position_risk`, `get_position_history`, `get_position_pnl_history`
* **Portfolio** — `get_portfolio_summary`, `get_portfolio_performance`, `get_portfolio_realized_pnl`, `get_portfolio_assets`, `get_portfolio_margin`
* **Margin accounts** — `get_margin_accounts`, `get_parent_margin_account_summary`, `get_available_collateral`, `get_parent_margin_account_movements`, and the `simulate_parent_margin_order_risk` / `simulate_risk_bucket_order_risk` risk previews
* **Account & market data** — `get_user_trades`, `get_funding_payments`, `get_pending_withdrawals`, and the public `get_pulse` news feed
All of these are read tools under the default `read` profile, except `close_position` (which requires `market-maker`) and the collateral-movement tools (`add_position_margin`, `reduce_position_margin`, and the parent- and risk-bucket transfers), which are fund-movement and require `full`. The `@deprecated` per-margin-account forms are intentionally not exposed; their non-deprecated parent/risk-bucket equivalents are. Tool input/output schemas mirror the SDK's camelCase ergonomic types and cross-field refinements. See the [full MCP tool list](/sdk/mcp-server#available-tools).
## Changed
### Batch order replacement is cancel-first
`sdk.trading.batchReplace` / `batchReplaceMarginLimitOrders` — and the Rust REST/gRPC `batch_replace_orders` — are now **cancel-first**: the matching engine cancels every item's original before placing any replacement, in request order. A two-sided quote can shift across its own resting prices in one call without tripping self-trade prevention, and a replacement may lock funds freed by any original in the batch.
Failure semantics changed with it. An item whose replacement fails to **place** (insufficient balance, self-trade against an order outside the batch, risk rejection) now reports the error with its original **already cancelled** — previously a failed item left its original resting. Items that fail validation, or whose original cannot be found, still leave the original untouched.
### MCP tool inventory generated from a manifest
The tool inventory in the MCP README is now generated from a single deterministic manifest (`src/manifest.ts`) that classifies every tool by category, auth mode, capability profile, network restriction, and side effect. The capability policy and the test profile buckets derive from this manifest, and a CI drift gate fails if the documented tool set diverges from the tools the server actually registers. No tool behavior changed.
## Fixed
### `Retry-After` HTTP-date parsing on `APIError`
`APIError.retryAfter` now also parses the HTTP-date form of the `Retry-After` response header, converting it to the remaining delay in **seconds** (past dates clamp to `0`); previously an HTTP-date was dropped. All three RFC 7231 HTTP-date formats are accepted (IMF-fixdate, RFC 850, and asctime), timestamps are built as UTC, and impossible calendar dates (e.g. 31 Feb) or malformed values are ignored rather than coerced into a bogus delay. The numeric form is unchanged. The value is in **seconds** — multiply by `1000` before passing it to `setTimeout`. See [error handling](/sdk/typescript/error-handling).
### `request_faucet` MCP tool
The `request_faucet` MCP tool was dead: it referenced the removed `accessToken` auth wiring and always failed with "No access token available". It now calls the current faucet SDK path (`sdk.faucet.mint`) and takes the required Cloudflare Turnstile `turnstileToken` input. The tool remains network-restricted and is never registered on `mainnet`.
### Session-key auth documented in client JSDoc
The WebSocket client (`subscribeUserOrders`, `subscribeBalances`, `subscribeMovements`, `sendAuthenticate`), the React hooks (`useUserBalances`, `useUserMovements`, `useUserOrders`), and the WebSocket balance/movement event types no longer reference JWT/token authentication in their JSDoc. They now document that the user is identified from the signed session-key `Authenticate` handshake over `WS-AUTH\n\n`, matching the tokenless v0.8.8 model, and that the handshake timestamp is bound into the signature and validated within a ±30s skew window (re-signed on every reconnect). Behavior and signatures are unchanged; these are documentation-only corrections.
## Upgrade
```bash theme={null}
npm install @0xmonaco/core@1.0.33 @0xmonaco/types@1.0.33 @0xmonaco/react@1.0.33
# or bun
bun add @0xmonaco/core@1.0.33 @0xmonaco/types@1.0.33 @0xmonaco/react@1.0.33
```
The MCP server ships at `1.0.33` too; it runs via `npx -y @0xmonaco/mcp-server`, so a restart of your assistant picks up the new version and the capability-profile behavior below.
Action items to review:
1. **Authenticated MCP sessions default to read-only.** After upgrading, an authenticated MCP server started without `MONACO_CAPABILITY` exposes read tools only — set `MONACO_CAPABILITY=market-maker` to place, cancel, and replace orders, or `full` for vault fund movement. `MONACO_NETWORK` is now required whenever a private key is set; the server no longer defaults to `mainnet`.
2. **Batch replace is cancel-first.** A replacement that fails to place now leaves its original **already cancelled**, not resting. Re-check any batch-replace error handling that assumed a failed item kept its original order live.
3. **`Retry-After` is in seconds.** `APIError.retryAfter` is delay-seconds (now also derived from HTTP-date headers); multiply by `1000` before passing it to `setTimeout`.
# V1.0.34
Source: https://docs.0xmonaco.com/releases/v1.0.34
# Monaco Protocol SDK v1.0.34
This is an additive release — no breaking changes. It adds custom (vanity) PitPass TraderCodes across the REST, gRPC, TypeScript, React, and MCP surfaces, so a wallet can claim a human-readable handle instead of only the wallet-derived default. It also exposes lifetime cost fields (`exitPrice`, `fundingPaid`, `feesPaid`) on terminal position rows, and hardens the MCP server so long-running agents keep their session alive and start cleanly on Node 18. Existing code keeps working unchanged; the new custom-handle methods and fields are opt-in.
## Added
### Custom (vanity) PitPass TraderCodes
A wallet can now claim a custom handle for its TraderCode instead of using only the wallet-derived default. Three new methods land across every client:
* `setTraderCode(code)` (`PUT /api/v1/pitpass/codes/me/custom`; gRPC `TraderCodeService.SetTraderCode`) — claim or edit your handle. The handle is stored exactly as entered (casing preserved), must be letter-first `[A-Za-z0-9_]` and 5–20 characters, and is validated client-side before sending; the server remains the authoritative gate. Rejections map to `409` (handle taken case-insensitively, or a recently-freed handle still in cooldown), `422` (validation, carrying a stable reason code), `429` (edit rate-limit), and `403` (the feature is disabled). Authenticated.
* `clearTraderCode()` (`DELETE /api/v1/pitpass/codes/me/custom`; gRPC `TraderCodeService.ClearTraderCode`) — drop the custom handle and revert to the wallet-derived default. Clearing also counts against the rolling-hour edit limit, so it can return `429` (rate-limited) or `403` (feature disabled). Authenticated.
* `checkTraderCodeAvailability(code)` (`GET /api/v1/pitpass/codes/available`; gRPC `TraderCodeService.CheckTraderCodeAvailability`) — a public advisory check that returns `available`, and when unavailable a `reason` (`taken`, `too_short`, `too_long`, `reserved`, `invalid_char`, or `looks_like_address`). It is rate-limited per client and returns `429` once a caller exceeds the allowance; use it to give live feedback in a claim form, not as the authoritative gate.
`getMyTraderCode()` now returns three additive fields alongside `code`: `customCode` (the handle as entered, or an empty string when the default is kept), `isCustom` (whether a custom handle is set), and `display` (the string to show users). React clients gain matching hooks — `useMyTraderCode`, `useSetTraderCode`, `useClearTraderCode`, and `useTraderCodeAvailability` — and MCP clients gain the `set_trader_code`, `clear_trader_code`, and `check_trader_code_availability` tools. See [TraderCodes](/developers/tradercodes) and the [TypeScript TraderCodes SDK](/sdk/typescript/pitpass).
### Lifetime cost fields on terminal positions
Terminal position rows (`CLOSED`, `LIQUIDATED`, `EXPIRED`) returned by `getPositions` / `getPosition` (REST `GET /api/v1/positions`, `GET /api/v1/positions/{positionId}`; gRPC `PositionsService.ListPositions` / `GetPosition`) now carry three optional fields:
* `exitPrice` (gRPC `exit_price`) — volume-weighted average price across the position's reducing executions, so a position closed over several fills reports its true blended exit rather than the last fill or the mark at close.
* `fundingPaid` (gRPC `funding_paid`) — lifetime funding paid, summed over the position's reducing executions (signed; negative means received).
* `feesPaid` (gRPC `fees_paid`) — closing-side trading fees paid (signed; negative means rebated); fees on opening fills are not included.
All three are omitted when the position's reducing executions do not cover its whole lifetime; there is no stored substitute for them (the terminal `markPrice` still carries the settled per-row exit). Open rows are unchanged. See [Positions](/sdk/typescript/positions).
## Fixed
### MCP server keeps its session alive
The MCP server now maintains its own Monaco session, so a long-running unattended agent no longer starts failing with `401`s once the session expires. `ensureAuthenticated` is expiry-aware: it refreshes the session — falling back to a fresh login if the session is dead — when the session is expired or within 60 seconds of expiry, rather than only logging in when no session exists. Idempotent read tools are additionally wrapped so a single auth-expired response triggers exactly one recovery and retry; order-placement and fund-movement tools are deliberately never auto-retried, so a whole-handler rerun can never double-execute a mutation. Concurrent calls share a single in-flight login/refresh instead of stampeding the auth service, and re-auth reuses the configured network, capability profile, and `clientId`. Unauthenticated servers (no private key) are unchanged and never attempt a login.
### MCP server starts cleanly on Node 18
Node 18 does not expose the WebCrypto global (`globalThis.crypto`) to script files, so `@0xmonaco/core`'s session-key generation threw "Secure randomness (crypto.getRandomValues) is unavailable" on the first authenticated call when the server was launched via `npx`/`node` on Node 18. The bin now polyfills WebCrypto from `node:crypto` when the global is missing or partial (Node ≥19 and Bun natives are left untouched), so authenticated tools work across every supported Node version.
## Upgrade
```bash theme={null}
npm install @0xmonaco/core@1.0.34 @0xmonaco/types@1.0.34 @0xmonaco/react@1.0.34
# or bun
bun add @0xmonaco/core@1.0.34 @0xmonaco/types@1.0.34 @0xmonaco/react@1.0.34
```
The MCP server ships at `1.0.34` too; it runs via `npx -y @0xmonaco/mcp-server`, so restarting your assistant picks up the new version, the session keep-alive, and the Node 18 fix.
Nothing in this release is breaking. If you show TraderCodes in your UI, switch from rendering `code` directly to `display`, and read `isCustom` / `customCode` to reflect a claimed handle.
# V1.0.36
Source: https://docs.0xmonaco.com/releases/v1.0.36
# Monaco Protocol SDK v1.0.36
This is an additive release — no breaking changes. It rolls up the consumer-facing changes shipped since v1.0.34. It adds registered deposit addresses with automatic sweeping — a public, keyless way to hand a user a stable deposit address that forwards every ERC20 deposit into the rollup — and exposes human-readable base/quote asset display names on every trading pair. It also hardens the MCP server: order create/replace tools are now flagged non-idempotent so an agent reconciles instead of blind-retrying after an ambiguous failure, and the MCP tool schemas were realigned with the underlying SDK methods so agent inputs are no longer silently dropped. Existing code keeps working unchanged; the new method and fields are opt-in.
## Added
### Registered deposit addresses with automatic sweeping
Every `(application, user)` pair has a deterministic on-chain deposit address. Registering the pair makes the backend monitor that address and automatically forward every ERC20 deposit into the rollup, crediting the user under the application — no per-deposit signature or transfer step.
* `sdk.sweeper.register({ clientId, userAddress })` (`POST /api/v1/sweeper/register`; gRPC `SweeperService.Register`; Rust REST `register_deposit_address`; Rust gRPC `SweeperServiceClient::register`) — register a pair. **Public — no session keypair required.** The reply returns immediately with the derived `sweeperAddress` and a `status` of `registered` (added by this call) or `already_registered` (known before). A one-time initial balance check — which sweeps anything already sitting on the address — runs in the background and is reported by `initialSweepCheck` (`false` right after a fresh registration, flipping once every pre-existing balance is swept). Idempotent and safe to retry: re-registering never re-triggers the initial check. The `clientId` is matched against existing applications by exact string, so an unknown client id — any respelling of a real one included — returns `404`; requests are lightly rate-limited per client (`429`), reject malformed input (`400`), and answer `503` when the sweeper is unconfigured or its resolver is down (nothing was registered, safe to retry).
`@0xmonaco/contracts` gains `predictSweeperAddress(...)` plus the `SWEEPER_ABI` and `SWEEPER_FACTORY_ABI` exports, so you can derive the same deposit address off-chain — and show or fund it before any contract exists there — without an RPC round-trip. See [Vault](/sdk/typescript/vault#registered-deposit-addresses).
### Human-readable asset display names on trading pairs
`TradingPair` now carries `baseAssetName` and `quoteAssetName` (wire `base_asset_name` / `quote_asset_name`) — the human-readable asset names from the asset registry (e.g. `"Robinhood Markets"` for `HOOD`, `"USDC"` for `USDC`) — alongside the existing ticker fields. They are populated on every pair returned by `getPaginatedTradingPairs`, `getTradingPair`, and `getTradingPairBySymbol` (REST `GET /api/v1/market/pairs` and `GET /api/v1/market/pairs/{trading_pair_id}`; gRPC `MarketService.ListTradingPairs` / `GetTradingPair`; Rust `base_asset_name` / `quote_asset_name`), all public. Add them to a market-search haystack so a query like "Robinhood" matches the `HOOD` market, and render them as display names next to tickers. Additive; existing fields are unchanged. See [Market Data](/sdk/typescript/market).
## Fixed
### MCP order tools are flagged non-idempotent
The `place_limit_order`, `place_market_order`, and `replace_order` MCP tool descriptions now state that order create/replace are non-idempotent and must not be blind-retried after an ambiguous outcome (a timeout, a network error, or a 5xx with no order returned). They direct the assistant to reconcile with `get_orders` / `get_order` before resubmitting, so an unattended agent no longer double-submits and opens duplicate live orders.
**Correction since this release.** The reconciliation surface named above was wrong. `get_orders` /
`get_order` are replica-backed and can omit an order that did land, and an order rejected *before* acceptance is never written to them at all (one rejected after acceptance is persisted with status `REJECTED`), so a missing row is not evidence the order failed. Reconcile on the order
WebSocket stream — which MCP has no tool for, so an ambiguous outcome cannot be resolved from inside
MCP and must be reported rather than retried. See [Client Order IDs](/developers/trade#client-order-ids).
### MCP tool schemas aligned with the SDK
The MCP tool schemas and descriptions were realigned with the underlying `@0xmonaco/core` methods so agent inputs are no longer silently dropped and risk is stated accurately:
* `get_trading_pairs`, `get_balances`, and `get_movements` now paginate with `page` / `pageSize` (the params the SDK actually reads) instead of the ignored `limit` / `offset`; `get_trading_pairs` also gained a `category` filter.
* `place_limit_order` and `place_market_order` gained a `marginMode` (`ISOLATED` / `CROSS`) option for margin orders.
* `place_market_order` and the batch-cancel tools now describe their real semantics: the mandatory 1,000 bps market-order price band (tightening-only slippage, possible partial fill or rejection) and the non-atomic, best-effort nature of batch cancels.
* Every tool now carries MCP `readOnlyHint` / `destructiveHint` annotations derived from the manifest, so clients can gate the mutating and fund-movement surface.
## Upgrade
```bash theme={null}
npm install @0xmonaco/core@1.0.36 @0xmonaco/types@1.0.36 @0xmonaco/react@1.0.36 @0xmonaco/contracts@1.0.36
# or bun
bun add @0xmonaco/core@1.0.36 @0xmonaco/types@1.0.36 @0xmonaco/react@1.0.36 @0xmonaco/contracts@1.0.36
```
The MCP server ships at `1.0.36` too; it runs via `npx -y @0xmonaco/mcp-server`, so restarting your assistant picks up the new version, the non-idempotent order-tool guidance, and the aligned tool schemas.
Nothing in this release is breaking. Two things worth adopting:
1. **Registered deposit addresses are public and keyless.** `sdk.sweeper.register` needs no session key — the deposit address's destination is fixed in its own bytecode, so a caller can only ever route funds to the user they already belong to. Give a user their stable address up front and let deposits sweep in automatically.
2. **Asset display names are available for search and UI.** Read `baseAssetName` / `quoteAssetName` off trading pairs to match markets by company or asset name and to render names beside tickers.
# V1.0.37
Source: https://docs.0xmonaco.com/releases/v1.0.37
# Monaco Protocol SDK v1.0.37
This is an additive release — no breaking changes. It adds a **post-only** flag to limit-order placement and replacement across every client. `postOnly` is a maker-only guarantee, not a new order type: a placement or replacement that would cross the book is rejected instead of matching, so a resting quote never accidentally takes. Existing code keeps working unchanged; the flag is opt-in. The one thing to know before adopting it is how a crossing rejection surfaces — a message match for single orders, a structured error code for batches.
## Added
### Post-only orders
Set `postOnly: true` alongside a normal limit placement or replacement to guarantee the order can only add liquidity. If the price would cross the book at admission time (`BUY` at or above the best ask, or `SELL` at or below the best bid), Monaco rejects the order instead of letting it take.
* Exposed on every limit-order surface: `sdk.trading.placeLimitOrder`, `replaceOrder`, `batchCreate`, and `batchReplace` (`@0xmonaco/core`), the `useTrade()` equivalents (`@0xmonaco/react`), and the REST `create_order` / `batch_create_orders` / `replace_order` / `batch_replace_orders` endpoints (`POST /api/v1/orders`, `POST /api/v1/orders/batch-create`, `PUT /api/v1/orders/{order_id}`, `POST /api/v1/orders/batch-replace`) and their gRPC `OrdersService` equivalents. The MCP `place_limit_order` and `replace_order` tools take an optional `postOnly` too.
* **Maker-only, not a new order type.** `postOnly` composes with `LIMIT` orders using `GTC` time in force only. It is rejected on `MARKET` orders and rejected when combined with `IOC` or `FOK` — both are immediate-or-cancel semantics that contradict a maker-only guarantee. It is not available on `placeMarketOrder`. The TypeScript schemas enforce these combinations client-side on `placeLimitOrder` and `batchCreate` so a bad combination fails fast without a round trip — but that client check is for fast feedback only; the server's own validation is authoritative and always enforced.
* **Crossing rejection.** A crossing `postOnly` order or replacement is rejected with REST `400 Bad Request` / gRPC `InvalidArgument`, message containing `"post-only order would cross"` (internally logged as `POST_ONLY_WOULD_CROSS`). For a single `placeLimitOrder` or `replaceOrder`, that code is **not** part of the response body — match on the message text. `batchCreate` and `batchReplace` are different: each per-item result carries a structured `error.code`, set to `POST_ONLY_WOULD_CROSS` for a post-only crossing rejection — match on `code` there.
* **Replace semantics.** `postOnly` is re-stated explicitly on every replacement; it is never inherited from the original order. A single `replaceOrder` validates `postOnly` *before* cancelling the original, so a rejected replacement leaves the original resting, untouched. `batchReplace` is cancel-first: a batch item whose `postOnly` replacement is rejected for crossing reports the error with its original **already cancelled**, matching the existing failed-to-place batch-replace contract — refresh your live orders before retrying.
* **Admission-time check only.** The crossing predicate is evaluated once, when the order is admitted (placed or replaced) — `postOnly` is not a perpetual anti-taker guarantee. Once a non-crossing `postOnly` order is admitted and rests, it behaves like an ordinary resting order of the same time in force: a later book move can trade against it like any other resting order. The flag is not stored on the order and not echoed back in order-detail responses.
See [Post-Only Orders](/developers/trade#post-only-orders) for the full crossing predicate and the replace / batch-replace resting-order contract.
**Corrections since this release.** The original wording is left in place above; three statements have been superseded:
* *"The flag is not stored on the order and not echoed back in order-detail responses"* — false since v1.0.38: `postOnly` is persisted on the order and returned on order detail and list responses, and since v1.0.51 it is also echoed on the order WebSocket events (the placement acknowledgement, taker fills, cancellations, and expiry). Absent means not post-only; the key is never sent as `false`.
* *"For a single `placeLimitOrder` or `replaceOrder`, that code is **not** part of the response body — match on the message text"* — superseded in v1.0.38: a crossing rejection now carries the stable error code `POST_ONLY_WOULD_CROSS` on every surface (the REST error envelope's `code` field, the gRPC `google.rpc.ErrorInfo` reason, and the batch per-item `error.code`). Matching the message substring still works but is a legacy fallback.
* *"a batch item whose `postOnly` replacement is rejected for crossing reports the error with its original **already cancelled**"* — superseded in v1.0.47: the crossing check now runs in the pre-cancel preflight, evaluated against the book as it will stand once the batch's own cancels are applied, so a refused item ordinarily leaves its original resting with the same order ID and queue position. See [Batch Replace](/developers/trade#batch-replace) for the narrow cases where the original is still lost.
## Upgrade
```bash theme={null}
npm install @0xmonaco/core@1.0.37 @0xmonaco/types@1.0.37 @0xmonaco/react@1.0.37 @0xmonaco/contracts@1.0.37
# or bun
bun add @0xmonaco/core@1.0.37 @0xmonaco/types@1.0.37 @0xmonaco/react@1.0.37 @0xmonaco/contracts@1.0.37
```
The MCP server ships at `1.0.37` too; it runs via `npx -y @0xmonaco/mcp-server`, so restarting your assistant picks up the new version and the `postOnly` tool inputs.
Nothing in this release is breaking. One thing worth adopting:
1. **Use `postOnly` for maker quoting.** Set `postOnly: true` on limit placements and replacements where an accidental take is worse than a rejected order you can immediately re-price — market-maker quoting, or rebuilding quotes right after a market reopens. Handle the crossing rejection by the message for single orders (`"post-only order would cross"`) and by `error.code === "POST_ONLY_WOULD_CROSS"` for batches, and remember `batchReplace` cancels first — a rejected item leaves no resting order behind.
# V1.0.38
Source: https://docs.0xmonaco.com/releases/v1.0.38
# Monaco Protocol SDK v1.0.38
This is an additive release — no breaking type changes. It brings the typed WebSocket surface up to par with the account APIs: dedicated `sdk.ws.positions` and `sdk.ws.liquidations` streams, and reconnect/resync callbacks you can now wire straight into the SDK config instead of dropping to the low-level factory. It also makes `postOnly` a durable, readable property of an order and gives single-order crossing rejections the same structured error code the batch endpoints already carried, adds an `order_id` filter to the gRPC user-trades history, and ships three PitPass React hooks. Two changes need a closer look before you upgrade: a set of **collateral withdrawal-gate corrections** that make `withdrawableCollateral` report a number a transfer-out will actually accept (expect a smaller figure than before for any account carrying PnL, funding, or an unrealized gain), and a **WebSocket order-event field-name correction** — read `tradingPair`, `filledQuantity`, and `fee` on order events, not the never-populated `symbol` / `executionQuantity` / `makerFee`.
## Added
### Typed positions and liquidations WebSocket streams
`sdk.ws.positions(handler, tradingPairId?)` and `sdk.ws.liquidations(handler, tradingPairId?)` (`@0xmonaco/core`) subscribe to the authenticated positions and liquidations channels, parse each raw server frame into a typed `PositionEvent` / `LiquidationEvent` (`@0xmonaco/types`), and return an unsubscribe function — mirroring the existing `orders` / `balances` / `movements` helpers instead of the generic `subscribe`. Both require the signed session-key handshake. Positions use a **conflating** stream — a lagging subscriber is fast-forwarded to the latest state, so there is no gap signal; treat `updatedAt` as monotonic per `positionId`. Liquidations use a **sequential** stream and shed a slow subscriber with close code `1013` — treat a `1013` close as the gap signal and resync from a fresh snapshot. Parsing is lenient on the free-form / enum string fields (`side`, `status`, `reason`, `accountState`) so an unexpected server value never drops a risk-critical event. See [WebSockets](/sdk/typescript/websockets#positions-channel).
### WebSocket reconnect/resync configuration on the SDK config
`SDKConfig` gains an optional `ws` object (`ws?: MonacoWebSocketConfig`): the `onStatusChange` / `onResync` callbacks and the reconnect knobs (`autoReconnect`, `maxReconnectAttempts`, `reconnectBaseDelayMs`, `connectionTimeoutMs`) are now threaded into the WebSocket client the SDK builds internally. Previously these were reachable only through the low-level `createMonacoWebSocket` factory, so an app using the SDK could not observe connection status or drive snapshot resyncs. Pass them in the SDK config — `new MonacoSDK({ ..., ws: { onStatusChange, onResync } })` — and `onStatusChange` fires on connect/disconnect/reconnect while `onResync` fires after an automatic reconnect resumes the live stream (with `slowClient: true` for a `1013` slow-client shed). The session keypair is intentionally not part of this surface — it continues to flow from `login`. `@0xmonaco/types` adds `MonacoWebSocketConfig`, `StatusHandler`, `ResyncHandler`, and `WebSocketResyncInfo`.
### `postOnly` on order reads and a structured single-order crossing code
`GetOrderResponse` — order detail and every order-list item — gains an optional `postOnly` boolean (wire `post_only`; gRPC field tag 42) recording whether the order was placed post-only. Like `reduceOnly`, it is absent (not `false`) when the order was not placed post-only, including orders placed before the flag was persisted; `postOnly` is now a durable property of the order rather than an admission-time-only input. Exposed on `sdk.trading.getOrder` / `getOrders` (`@0xmonaco/core`), REST `GET /api/v1/orders` and `GET /api/v1/orders/{order_id}`, and gRPC `OrdersService.GetOrder` / `ListOrders`.
Single-order rejections that originate in the matching engine now carry the engine's stable code, so single-order callers get the same structured contract the batch endpoints always had. A post-only order or replacement that would cross the book is rejected with the code `POST_ONLY_WOULD_CROSS` — on REST it rides the error envelope's optional `code` field; on gRPC it is a `google.rpc.ErrorInfo` status detail with `reason` set to the code and `domain` set to `matching-engine.0xmonaco.com`. Match on the structured code; the `"post-only order would cross"` message substring still works but is now a legacy fallback. `error`, `message`, and the HTTP/gRPC statuses are unchanged, and `code` is omitted entirely when no engine code applies. The MCP `place_limit_order` and `replace_order` tool descriptions were updated to state this contract.
### `order_id` filter on gRPC user-trade history
`TradesService.ListUserTrades` (`/monaco.api.trades.TradesService/ListUserTrades`, authenticated, gRPC-only) adds an optional `order_id` string (an order UUID). When present, the response includes only trades where that order placed the caller's own side of the fill, across hot and archived history and in both pagination modes. `ListUserTradesResponse` is unchanged. Adding or changing `order_id` invalidates an existing page token — restart the cursor walk with an empty token.
### PitPass React hooks
`@0xmonaco/react` adds `useRewardsBalance` (`getRewardsBalance`), `useTraderCodeInfo` (`getTraderCodeInfo`), and `useTransferRewards` (`transferRewards`) — each a thin `useCallback` wrapper over the matching `sdk.pitpass.*` method that resolves the client from the `MonacoProvider` context. `useRewardsBalance` and `useTransferRewards` require authentication; `useTraderCodeInfo` is a public referral-code lookup. See [PitPass hooks](/sdk/react/hooks/use-pitpass).
## Fixed
### `withdrawableCollateral` now reports a number a transfer-out will accept
`withdrawableCollateral` (on margin account and risk bucket summaries — `GET /api/v1/margin/accounts`, `GET /api/v1/margin/accounts/{marginAccountId}`, `GET /api/v1/margin/parent-margin-account`), `newWithdrawableCollateral` (on every collateral transfer-in/transfer-out response under `/api/v1/margin/`), and `marginAvailableCollateral` (`GET /api/v1/margin/available-collateral`) previously reported `freeCollateral` — an equity figure that credits realized PnL, accrued funding, and unrealized mark-to-market gains that are not principal a transfer can move. The published number could far exceed anything a transfer-out would accept; a risk bucket holding realized PnL against roughly no allocated collateral advertised its whole balance as withdrawable while rejecting every transfer. All three now report the largest amount the exchange's withdrawal gate will actually accept — for a risk bucket, capped by its allocated collateral (net of its own resting-order and position margin and funding, plus realized PnL, counting an unrealized loss but never an unrealized gain) and by the parent account's own limit; for a parent margin account, capped by unearmarked deposited collateral and by `freeCollateral` less any unrealized gain. The field types, positions, and status codes are unchanged — only the value changed. **Size transfers and withdrawals from this field**; `freeCollateral` is unchanged and remains the equity/new-position headroom figure. Expect a smaller number than before for any account or bucket carrying realized PnL, accrued funding, or an unrealized gain.
The same correction closes several related withdrawal-gate gaps, all with unchanged types and status codes: an unrealized gain no longer funds a withdrawal from a parent margin account; a bucket-scoped transfer-out is no longer wrongly refused on a fully-allocated account (the release the request itself performs is now credited); and a partial withdrawal can no longer strip a losing risk bucket a piece at a time below the loss it has taken, which had let the containment step bill the difference to the insurance fund. Refusals in these cases carry a distinct, bucket-aware message so an automated client does not read them as a shortfall and top up collateral to no effect.
### WebSocket order-event field names corrected
Order events on `ws.events.orders` (`OrderFilled`, `OrderPartiallyFilled`, `OrderCancelled`, and the shared common fields) declared `symbol`, `executionQuantity`, and `makerFee`, but the matching engine has always sent `trading_pair`, `filled_quantity`, and `fee` (camelCased to `tradingPair`, `filledQuantity`, and `fee`). Consumers reading the old names received `undefined` — **use `tradingPair`, `filledQuantity`, and `fee`.** `filledQuantity` is the quantity filled in this event: the cumulative order fill for a taker, the current trade's quantity for a maker, keyed by `role`. The RAW (integer, base-unit) precision fields the wire already carried are now typed too — `quantityRaw`, `filledQuantityRaw`, `remainingQuantityRaw`, and `totalFilledRaw`. The `executedAt` runtime value is now RFC3339 with a `Z` suffix (e.g. `2026-01-02T03:04:05Z`) so `new Date(executedAt)` parses an unambiguous UTC instant; the TypeScript type is unchanged (`string`). `@0xmonaco/react`'s `useUserOrders` now reads its trading-pair fallback from `data.tradingPair` instead of the never-populated `data.symbol`.
### `applications/config` 401 description corrected
The documented `401` response on `GET /api/v1/applications/config` (`ApplicationsService.GetConfig`) was corrected from "Unauthorized - Invalid or missing JWT" to "Authentication required". The endpoint is session-authenticated (signed ed25519 session key, `monacoSignature`) and never used JWT/Bearer tokens. Description-only — no type shapes, status codes, or security requirements change.
## Upgrade
```bash theme={null}
npm install @0xmonaco/core@1.0.38 @0xmonaco/types@1.0.38 @0xmonaco/react@1.0.38 @0xmonaco/contracts@1.0.38
# or bun
bun add @0xmonaco/core@1.0.38 @0xmonaco/types@1.0.38 @0xmonaco/react@1.0.38 @0xmonaco/contracts@1.0.38
```
The MCP server ships at `1.0.38` too; it runs via `npx -y @0xmonaco/mcp-server`, so restarting your assistant picks up the new version and the updated post-only tool descriptions.
Nothing in this release is breaking. Four things worth acting on:
1. **Adopt the typed positions/liquidations streams.** Use `sdk.ws.positions` / `sdk.ws.liquidations` for live risk state, and handle a `1013` liquidations close as a gap signal — resync from a REST snapshot.
2. **Wire reconnect handling through the SDK config.** Pass `ws: { onStatusChange, onResync }` to `new MonacoSDK({ ... })` to observe status and resync after reconnects without the low-level factory.
3. **Match on `POST_ONLY_WOULD_CROSS` for single orders.** Read the structured `code` (REST envelope) / `ErrorInfo.reason` (gRPC) instead of the message substring, and read `postOnly` off order detail/list reads.
4. **Re-check withdrawal sizing.** Size transfer-outs and withdrawals from the corrected `withdrawableCollateral` / `marginAvailableCollateral`, and fix any WebSocket order-event consumer reading `symbol` / `executionQuantity` / `makerFee`.
# V1.0.39
Source: https://docs.0xmonaco.com/releases/v1.0.39
# Monaco Protocol SDK v1.0.39
This release retires two long-dead response fields, adds a delegated-owner startup mode to the MCP server, and clarifies the React session-lifecycle hook's vocabulary. The one breaking change is narrow: `getPaginatedOrders` and `getPaginatedUserMovements` no longer carry the `latest_orders` / `latest_movements` fields — a live-engine hot-cache shadow the backend stopped populating in v0.8.7, so every caller has received `undefined` for them since. The one thing to act on before upgrading: if any code reads `latest_orders` / `latest_movements` off those paginated responses, switch to the `orders` / `movements` arrays.
## Breaking
### `latest_orders` / `latest_movements` removed from paginated responses
`GetPaginatedOrdersResponse` no longer declares the optional `latest_orders` field and `GetPaginatedUserMovementsResponse` no longer declares the optional `latest_movements` field (`@0xmonaco/types`, surfaced through `sdk.trading.getPaginatedOrders` and `sdk.profile.getPaginatedUserMovements` in `@0xmonaco/core`). These were a live-engine (Redis) hot-cache shadow, only ever populated on page 1, that the backend stopped writing in v0.8.7 — so every caller has received `undefined` for both fields since then, and neither was part of the wire schema.
`@0xmonaco/react`'s `useUserOrders` and `useUserMovements` hooks previously seeded their initial state from those fields and now build it from the paginated `orders` / `movements` arrays alone; both hooks' return shapes are unchanged.
**Migration.** Read the paginated `orders` / `movements` arrays directly. On a reconnect or resync, refetch a fresh snapshot rather than relying on a live-cache field for the newest rows.
## Added
### Delegated-owner mode for the MCP server
The MCP server (`@0xmonaco/mcp-server`) gains `MONACO_DELEGATED_OWNER_USER_ID`, a startup option that runs the server on behalf of a delegated **owner** account instead of the configured wallet's own account. The `MONACO_PRIVATE_KEY` wallet acts as the **agent**: on startup it authenticates as itself, then adopts an owner-scoped session (`loginAsDelegatedOwner`), so every subsequent tool call — reads and trades — acts on the owner account, with the agent wallet preserved as the audit actor. The owner must have registered the agent wallet with an active [delegation policy](/developers/delegated-agents); the server cannot self-assert ownership.
It is fail-closed: `MONACO_DELEGATED_OWNER_USER_ID` requires `MONACO_PRIVATE_KEY` and `MONACO_CLIENT_ID` (the agent must authenticate as itself first), a present-but-blank value is rejected, and a failed adoption never falls back to trading as the agent's own account. Fund-movement tools still require the `full` capability, and delegated sessions cannot withdraw owner collateral. Raw delegated-session creation is intentionally not exposed as a tool — owner adoption is a startup option, not an autonomous agent action. See [Delegated owner mode](/sdk/mcp-server#delegated-owner-mode).
## Fixed
### `useTokenLifecycle` documentation clarified to session-key vocabulary
`@0xmonaco/react`'s `useTokenLifecycle` hook has its JSDoc, inline comments, and log strings rewritten to describe the authenticated **session** — the wallet-authorized ed25519 keypair returned by `login()`, persisted and refreshed via `sdk.refreshAuth()` — instead of implying JWT/bearer tokens. Monaco is a tokenless session-key exchange; there are no separate access or refresh tokens. The exported members and config fields (`initializeTokens`, `clearTokens`, `refreshTokens`, `isTokenExpired`, `persistTokens`) keep their existing "token" spelling as retained legacy compatibility names, so the public API is unchanged. The hook's reference page is now linked from the docs navigation.
## Upgrade
```bash theme={null}
npm install @0xmonaco/core@1.0.39 @0xmonaco/types@1.0.39 @0xmonaco/react@1.0.39 @0xmonaco/contracts@1.0.39
# or bun
bun add @0xmonaco/core@1.0.39 @0xmonaco/types@1.0.39 @0xmonaco/react@1.0.39 @0xmonaco/contracts@1.0.39
```
The MCP server ships at `1.0.39` too; it runs via `npx -y @0xmonaco/mcp-server`, so restarting your assistant picks up the new version and the delegated-owner startup option.
The only breaking part of this release is the removed cache fields. One thing to act on:
1. **Stop reading `latest_orders` / `latest_movements`.** If any code reads those fields off `getPaginatedOrders` / `getPaginatedUserMovements` (or off the raw responses behind `useUserOrders` / `useUserMovements`), switch to the paginated `orders` / `movements` arrays; on a reconnect or resync, refetch a fresh snapshot.
# V1.0.40
Source: https://docs.0xmonaco.com/releases/v1.0.40
# Monaco Protocol SDK v1.0.40
This is a large release. The headline is a collateral-model change: the two position-margin methods that sourced collateral from the spot wallet are **removed**, and adjusting a position's margin now flows through its risk bucket and the parent margin account. Alongside it, the WebSocket surface is brought fully in line with the wire — a **breaking casing change** on the `positions` channel, a locked cross-language drift gate, new `market_stats` channels, and richer `balance_update` / movement / position events — the market-data endpoints gain dollarized open-interest and volume figures, and the candlestick endpoint finally honors its own bounds. Two things to act on before upgrading: migrate off `addPositionMargin` / `reducePositionMargin`, and update any WebSocket position consumer that matches on the PascalCase `Open` status.
## Breaking
### `addPositionMargin` / `reducePositionMargin` removed — adjust margin through the risk bucket
`positions.addPositionMargin()` and `positions.reducePositionMargin()` (`@0xmonaco/core`), the `perpRoutes.positions.addMargin` / `reduceMargin` route helpers, `usePositions()`'s `addPositionMargin` / `reducePositionMargin` (`@0xmonaco/react`), the `add_position_margin` / `reduce_position_margin` MCP tools, the REST endpoints `POST /api/v1/positions/{position_id}/margin/add` and `.../margin/reduce`, and the gRPC `PositionsService.AddPositionMargin` / `ReducePositionMargin` RPCs are all removed, along with the `AddPositionMarginRequest` / `ReducePositionMarginRequest` / `PositionMarginResponse` types and their validation schemas.
Both sourced collateral from the **spot wallet**, so collateral already held in the parent margin account could not be deployed to a position at all, and releasing margin pushed it out of the margin account entirely rather than back to the parent.
**Migration.** Adjust a position's margin through its risk bucket, moving collateral between the bucket and the **parent margin account** — neither leg touches the spot wallet. Fund the parent first if it has no free collateral.
* **Allocate** — `transferCollateralToRiskBucket({ asset, amount, tradingPairId, marginMode: "ISOLATED" })`
* **Release** — `transferCollateralFromMarginAccount(parentMarginAccountId, { asset, amount, tradingPairId })`
### WebSocket position status is now SCREAMING\_SNAKE\_CASE
Position events on the `positions` channel now carry `status` as `OPEN`, `LIQUIDATING` (a closing position), `CLOSED`, `LIQUIDATED`, or `EXPIRED` — matching the REST position status strings and the published `@0xmonaco/types` contract exactly — instead of the previous PascalCase (`Open`), which diverged from REST for the same field. `PositionEventStatus` has been narrowed accordingly. A consumer that matched on `"Open"` must switch to `"OPEN"`. The position event data also now carries optional `tradeId` and `orderId`, referencing the trade and/or order that caused the update (omitted when not applicable).
## Added
### `marginMode` on collateral transfer-out — releasing from a cross bucket
`marginAccounts.transferCollateralFromMarginAccount()` (`@0xmonaco/core`; `TransferCollateralRequest` in `@0xmonaco/types`; REST `POST /api/v1/margin/accounts/{margin_account_id}/collateral/transfer-out`; gRPC `TransferCollateralFromMarginAccountRequest.margin_mode`) accepts an optional `marginMode`. A cross risk bucket has no trading pair, so `tradingPairId` could not name it and there was no way to address a cross bucket on a transfer-out at all — collateral could enter a cross bucket and never leave. `marginMode: "CROSS"` selects the cross bucket, and the release lands in the parent margin account like every other bucket release. The two selectors are mutually exclusive: `marginMode: "CROSS"` together with `tradingPairId` is rejected client-side.
### Dollarized open interest and 24h volume on market-data endpoints
`getPerpMarketSummary` now returns `openInterestBase`, `openInterestNotional`, and `quoteVolume24h`, and `getMarketMetadata` now returns `quoteVolume24h` (gRPC `GetPerpMarketSummaryResponse` / `GetMarketMetadataResponse`). The perp summary previously exposed only base-asset `openInterest` and base-asset `volume24h`, so labelling either as USDC was wrong by roughly the mark price. `openInterestNotional` is `openInterestBase × mark price` (falling back to the latest close), and `quoteVolume24h` is the dollar figure directly; `openInterest` remains a base-asset alias of `openInterestBase`.
`getScreener` (`GET /api/v1/market/screener`) additionally returns `openInterestBase` and `openInterestNotional` on every `ScreenerItem`, so a client can read all pairs' open interest in one call instead of a per-pair `getPerpMarketSummary` fan-out. Both are `null` for spot pairs, and a margin pair with no open positions reports `"0"`. The screener notional is valued at the pair's latest trade price (a snapshot); the mark-accurate notional lives on `getPerpMarketSummary` / `getOpenInterest`.
### `market_stats` WebSocket channels
A new public per-market channel — subscribe with `market_stats:` — delivers periodic per-market frames: index / mark / mid prices, best bid/ask, open interest and its limit, last trade price, 24h volume / high / low and both absolute and percentage price change, and funding state (current rate, estimated next rate, next funding timestamp, premium, and clamp bounds).
A companion all-markets summary channel — subscribe with `market_stats` (or `market_stats:*`) — delivers, on a slower cadence, a single frame carrying a reduced per-market summary for every perp market: trading pair id, symbol, last trade price, mark price, absolute and percentage 24h price change, 24h base/quote volume and high/low, open interest, current funding rate, next funding timestamp, and a `market_status` label (`TRADABLE`, `POST_ONLY`, `HALTED`, `NOT_READY`, or `UNKNOWN`). Both channels are public — no session handshake required. SPOT markets flow through both, carrying orderbook, last trade, 24h stats, and a `TRADABLE` / `HALTED` label, with the perp-only fields (index/mark price, open interest and limit, funding rate/timestamp/premium, base interest rate, funding clamps) omitted.
### Richer position, balance, and movement events
* Position events now also emit on liquidation-worker and ADL closes and for counterparties impacted by a fill: a terminal `LIQUIDATED` update — with `reason` `"liquidation"` for a maintenance-margin liquidation and `"adl"` for an auto-deleverage close — when a position is force-closed, plus live `OPEN` refreshes for surviving or partially-reduced positions. Funding settlements now also emit a `position_update` with `reason: "funding"` carrying the position's post-settlement state (status `OPEN`, unchanged size), so a subscriber can attribute a live position change to funding.
* The `balance_update` `reason` union gains `fee`, `rebate`, `funding`, `margin_deposit`, `liquidation`, and `adl`, so a collateral change can be attributed to a fee charged, a maker rebate credited, a funding settlement, a deposit routed into margin collateral, a liquidation, or an auto-deleverage. Perp/margin fills on the order-fill path now emit a `trade`-reason collateral frame for **every** margin account the fill touched — the resting maker(s), not only the aggressing taker — and a separate `rebate` frame when a maker earned a rebate (negative maker fee). Each frame carries the user's resulting balance for one token (the envelope's `userId` plus the payload's `tokenAddress` — the user's aggregate for that token, with no per-margin-account key): it is a terminal-state snapshot, not an additive delta, so apply the latest balance per `(userId, tokenAddress)` rather than summing the `reason` frames.
* Funding settlements surface on the `movements` channel with `transactionType: "funding"`, reconciling with the matching `balance_update` whose `reason` is `funding`.
## Changed
### A margin shortfall is no longer reported as an insufficient balance
`InsufficientInitialMargin` and `InsufficientFreeCollateral` previously surfaced as `Insufficient balance: required=..., available=...`. They now carry the risk engine's own wording:
```
insufficient initial margin: required=..., available=...
insufficient free collateral: requested=..., available=...
```
The HTTP status and error type are unchanged (`400` / invalid input); only the message differs. Clients that match on the message string to detect a funding problem must be updated — the account's wallet is not short, its margin is, and depositing does not fix a leverage or position-size problem. Genuine wallet shortfalls are untouched and still read `Insufficient balance`.
### The candlestick endpoint now honors `startTime`, `endTime`, and `limit`
`GET /api/v1/market/pairs/charts/candlestick/{tradingPairId}/{interval}` (`getCandlesticks`) accepted all three parameters and then returned far more data than either bound allowed — a 20-minute window could return 350 bars spanning 5.8 hours, 330 of them before `startTime`. Two behaviors caused it, and both are gone:
* **The window is now absolute.** A range holding fewer bars than `limit` was topped up with history from before `startTime`; it now comes back short instead, and no bar outside `[startTime, endTime]` is ever returned. Both bounds are inclusive.
* **`limit` is now a hard cap.** Gap-filled flat bars (buckets with no trades, carrying the previous close) are now counted towards `limit`, and the bar the request anchored on is the one that survives the cap — a forward page (`startTime`) still begins at `startTime`, while a latest or backward page still ends at its newest bar.
Unchanged: `limit` still defaults to 350 and clamps to 500, bars are still ascending oldest-to-newest, and forward / backward pagination still walk from `startTime` / end at `endTime`. Clients that relied on the old padding to fill a chart should widen their requested range, or drop `startTime` to get the most recent `limit` bars.
### Consistent order-event fill quantities
Order WebSocket events now report a consistent filled-quantity representation across taker, maker, and IOC/cancel events. `filledQuantity` is always the cumulative order fill (a maker event previously put only that trade's quantity there); a new optional `lastFillQuantity` / `lastFillQuantityRaw` carries the incremental fill contributed by the event (the summed trades for a taker, the single trade for a maker) and is omitted when the event carries no fill. The IOC/market partial-cancel event now also carries `filledQuantity` / `filledQuantityRaw`, mirroring the fill events; its legacy `totalFilled` / `totalFilledRaw` remain as aliases. The full order-cancelled payload's `cancelledAt` is now RFC3339 with a `Z` suffix, matching `executedAt`, and its typed shape now also declares `averageFillPrice` (already present on the wire).
### Batch order requests are capped at 100 items
Batch create / replace / cancel requests are now capped at 100 items per request (Zenith audit #11). `BatchCreateOrdersSchema` and `BatchReplaceOrdersSchema` reject arrays longer than 100 with a validation error before the request is sent, and the new `MAX_BATCH_ORDER_ITEMS` constant (100) is exported from `validation/trading`. The REST and gRPC APIs reject an oversized batch create/replace/cancel with a `400` and the message `At most 100 orders per batch request`; the server-enumerated cancel-all endpoint is unaffected. Under sequencer overload the APIs may now return a retryable `503` with error code `OVERLOADED` instead of queueing requests unboundedly.
### `tokenSymbol` populated on deposit/withdrawal balance events
The `balance_update` event (authenticated `balances` channel) now carries the registry-configured `tokenSymbol` (e.g. `"USDC"`) on `deposit` and `withdrawal` events; it was previously `null` on those reasons, forcing clients to fall back to `tokenAddress`. The field type is unchanged (`string | null`) — `null` now only appears for tokens without a registry entry.
### WebSocket event surface locked with a drift gate
Every MonacoWebSocket channel (`orders`, `userOrders`, `orderbook`, `ohlcv`, `trades`, `movements`, `balances`, `conditionalOrders`, `positions`, `liquidations`) now has a golden wire-format fixture and a test asserting `@0xmonaco/types` declares a field for every wire key the SDK surfaces, plus a coverage assertion that fails if a future channel lands with a loose handler or without a drift test. This keeps the typed event surface in sync with the Rust wire format; the visible effect this release is the movement-event fields below and the order/position event corrections above.
## Fixed
### WebSocket movement event parser reads the current wire
The movement event parser now reads the serialized `token` key (with a `token_address` fallback) and surfaces `status`, `assetId`, and `blockNumber` from the wire. Previously it read only the legacy `token_address` alias — absent from the current wire — and dropped those fields. `UserMovementEventData` now declares `token` / `status` / `assetId` / `blockNumber` (`token` optional; `tokenAddress` kept as a populated back-compat alias).
### IOC position closes accept and require `limitPrice`
`ClosePositionSchema` and the `close-position` MCP tool now accept — and require — `limitPrice` on IOC close requests, matching the server contract. The server executes an IOC close as a `LIMIT`+`IOC` order and rejects it without a price, but the schema previously rejected any IOC request that carried one, making IOC closes impossible through the SDK. `limitPrice` is now required for `LIMIT` and `IOC` closes and forbidden only for `MARKET`.
## Deprecated
### `TIME_IN_FORCE_GTD` (`"GTD"`)
`GTD` is declared in the protos but was never implemented, and order admission has always rejected it. Submitting `timeInForce: "GTD"` on create or batch-create fails with HTTP `400` / gRPC `INVALID_ARGUMENT` and the message `Invalid timeInForce: Time in force must be GTC, IOC, or FOK` — unchanged behavior; only the declaration is now marked deprecated, and the documentation that incorrectly advertised `GTD` as supported has been corrected to `GTC`. The enum value is retained so generated clients keep compiling. For an order that expires at a set time, send `timeInForce: "GTC"` with `expirationDate` — the GTC expiration job enforces it.
## Upgrade
```bash theme={null}
npm install @0xmonaco/core@1.0.40 @0xmonaco/types@1.0.40 @0xmonaco/react@1.0.40 @0xmonaco/contracts@1.0.40
# or bun
bun add @0xmonaco/core@1.0.40 @0xmonaco/types@1.0.40 @0xmonaco/react@1.0.40 @0xmonaco/contracts@1.0.40
```
The MCP server ships at `1.0.40` too; it runs via `npx -y @0xmonaco/mcp-server`, so restarting your assistant picks up the new version, the removed position-margin tools, and the corrected `close-position` contract.
The two breaking changes and the things worth acting on:
1. **Migrate off `addPositionMargin` / `reducePositionMargin`.** Allocate with `transferCollateralToRiskBucket({ ..., marginMode: "ISOLATED" })` and release with `transferCollateralFromMarginAccount(parentMarginAccountId, { ..., tradingPairId })`; both move collateral between the risk bucket and the parent margin account, never the spot wallet.
2. **Update WebSocket position consumers for the new status casing.** Match on `OPEN` / `LIQUIDATING` / `CLOSED` / `LIQUIDATED` / `EXPIRED`, not the old PascalCase `Open`.
3. **Re-check margin-shortfall handling.** Match on the new `insufficient initial margin` / `insufficient free collateral` messages, not `Insufficient balance`, when detecting a margin (not wallet) shortfall.
4. **Size batch requests to 100 items** and handle a retryable `503` `OVERLOADED`, and pass `limitPrice` on IOC position closes.
# V1.0.41
Source: https://docs.0xmonaco.com/releases/v1.0.41
# Monaco Protocol SDK v1.0.41
This release adds a client-assigned `clientOrderId` correlation handle across order placement, replacement, and every order read and WebSocket event; gives order-attached take-profit / stop-loss legs their own sizing so they no longer freeze at the entry order's quantity; adds a new authenticated `account` WebSocket channel streaming a margin account's live health; and emits a `movements` ledger entry for on-chain deposits and withdrawals so that channel lines up with `balances`. It also fixes — and corrects the 1.0.40 migration guidance for — funding an isolated risk bucket, which was silently ratcheting a sole open position's stored margin. One thing to act on: if you followed the 1.0.40 note and used `transferCollateralToRiskBucket` to *increase a position's* margin, that is not what it does — see the fix below.
## Added
### `clientOrderId` — a correlation handle on order placement
`placeLimitOrder`, `placeMarketOrder`, `replaceOrder`, `batchCreate` and `batchReplace` (`@0xmonaco/core`; `POST /api/v1/orders`, `PUT /api/v1/orders/{orderId}`, `POST /api/v1/orders/batch-create` and `.../batch-replace`; gRPC `CreateOrderRequest`, `ReplaceOrderRequest`, `BatchCreateOrderItem`, `BatchReplaceOrderItem`; the `place_limit_order` / `place_market_order` / `replace_order` MCP tools) now accept an optional `clientOrderId` — a label you choose that Monaco stores and echoes back. It is up to 64 characters from `A-Za-z0-9._:-`; surrounding whitespace is trimmed and a blank string is treated as absent. A longer value, or one containing any other character, is rejected with HTTP `400` / gRPC `INVALID_ARGUMENT`; on batch-create the rejection is per item with `error.code` `INVALID_CLIENT_ORDER_ID`, while batch-replace validates every item up front and rejects the whole request.
The handle is echoed on order detail and order list responses (`clientOrderId`, absent when the order carried none) and on every `orders` WebSocket event — placed, taker fill, maker fill, cancelled, rejected and expired. A maker fill carries the resting order's own handle, so a filled quote identifies itself without a follow-up read, and a submit whose HTTP response was lost can be resolved from the stream alone.
It is a **correlation handle, not an idempotency key**: Monaco does not de-duplicate on it and does not enforce uniqueness, so sending the same value twice places two orders. It does not make a lost-response retry safe — reconcile against the order list first, matching on `clientOrderId`, and resubmit only if nothing landed. A replacement is a new order with a new id: it carries its own `clientOrderId` and inherits nothing from the original, the same way `postOnly` already behaves. Orders placed before this release report no `clientOrderId`.
**Corrections since this release.** Three statements above have been superseded. The original wording is left in place; each correction is listed here:
* *"does not enforce uniqueness"* — [v1.0.42](/releases/v1.0.42) added a uniqueness guard, but only among your **resting** orders. An order that never rests takes no handle.
* *"reconcile against the order list"* — the order list is served asynchronously from a replica and can omit an order that did land, and an order rejected *before* acceptance is never written to it at all. Reconcile on the order WebSocket stream (`sdk.ws.userOrders`) instead. See [Client Order IDs](/developers/trade#client-order-ids).
* *"on every `orders` WebSocket event — placed, taker fill, maker fill, cancelled, rejected and expired"* — accurate for one class of rejection only. An order rejected *after* acceptance — a `LIMIT` with `IOC`/`FOK` that cannot fill, or a `MARKET` order whose apparent liquidity is invalidated by maker-risk revalidation — returns `2xx` with `matchResult.status` `REJECTED` and does emit `OrderRejected`. A create rejected *before* acceptance — malformed, post-only would cross, handle conflict, or a plain `MARKET` submit with no liquidity, rejected during pricing — fails the request, emits nothing, and is never written to order history either (an order rejected *after* acceptance is persisted with status `REJECTED`). And the retained claim above that a lost response "can be resolved from the stream alone" is retracted: a matching event is positive proof the order landed, but the stream is not replayable and its fan-out is non-persistent, so silence is never evidence it did not — absent a matching event the outcome is unresolved.
### Order-attached take-profit / stop-loss legs size independently
The `takeProfit` / `stopLoss` legs attached to an order at placement (`ParentTpSlLeg`; `placeLimitOrder` / `placeMarketOrder` / `batchCreate` in `@0xmonaco/core`, the matching REST and gRPC order requests) gain `quantity` and `closePosition`, the same sizing contract the position-attached `TpSlLeg` already carries. Previously an order-attached leg had no way to express its own size and was frozen at the parent order's quantity, so a position that later grew past it kept a stop covering only part of what was held while still reading as attached. Omit both fields (or set `closePosition: true`) to resolve the full live position size at trigger time, or name a `quantity` for a deliberate partial that never resizes; the two are mutually exclusive, and `quantity` is required when `closePosition` is `false`.
### `account` WebSocket channel — live margin-account health
A new authenticated channel — subscribe with `account` — carries a margin account's health: equity, collateral, free collateral, margin ratio, and maintenance requirement (all decimal strings), plus the margin account id and update timestamp, under an `account_update` event. The channel routing and the `AccountEvent` type in `@0xmonaco/types` ship in this release so integrators can build against the contract, but **the server-side producer that emits these frames is gated off by default and not yet enabled in the deployed environments** — subscribing succeeds and the wire shape is stable, but no frames arrive yet. When the producer is enabled its lag policy is *conflate* (a slow subscriber is fast-forwarded to the latest state) and it republishes an unchanged frame as a keepalive after a bounded quiet interval, so treat repeats as keepalives and each frame as a terminal-state snapshot per margin account, not an additive delta. There is no `sdk.ws` helper, so subscribe over the raw WebSocket protocol with a `Subscribe` frame naming the `account` channel; the raw wire is **snake\_case** (`event_type`, `user_id`, `margin_account_id`, `free_collateral`, `margin_ratio`, `maintenance_requirement`, `updated_at`) while the `AccountEvent` type is the camelCased ergonomic shape nothing converts to on this channel. See the [WebSocket reference](/reference/websockets#account) for the exact wire frame.
### `movements` frames for on-chain deposits and withdrawals
On-chain deposits and withdrawals now emit a `user_movement` frame on the `movements` channel, co-ordered with their `balance_update` on the `balances` channel. Every deposit/withdrawal balance change now has a matching `movements` ledger entry — transaction type `deposit` or `withdrawal`, carrying the amount, token, symbol, and balance snapshot — so the two streams line up; previously a deposit or withdrawal produced a `balances` frame with no corresponding `movements` entry. The `UserMovement` wire shape is unchanged (`transactionType` is a free-form lowercase string); these are additional transaction types on the existing channel.
## Fixed
### Funding an isolated risk bucket no longer ratchets a sole position's margin
Funding an isolated risk bucket through `transferCollateralToRiskBucket()` (`@0xmonaco/core`; the `transfer_collateral_to_risk_bucket` MCP tool; REST `POST /api/v1/margin/risk-buckets/collateral/transfer-in`; gRPC `TransferCollateralToRiskBucket`) no longer credits the bucket's sole open position's stored margin. It now always allocates reusable bucket collateral, as documented. Previously, when the bucket held exactly one open position, every transfer was silently treated as Adjust Margin, so routine bucket funding permanently ratcheted the position's initial margin and collateral requirement instead of leaving the collateral redeployable.
**This corrects the 1.0.40 migration note**, which presented `transferCollateralToRiskBucket` as the `addPositionMargin` replacement. It is not: it funds reusable bucket collateral. To explicitly increase a sole open position's stored margin, use the pair-scoped legacy transfer `transferCollateralToMarginAccount(parentMarginAccountId, { asset, amount, tradingPairId })` (`@0xmonaco/core`; REST `POST /api/v1/margin/accounts/{marginAccountId}/collateral/transfer-in` with `tradingPairId`; gRPC `TransferCollateralToMarginAccount`). There is no MCP tool for this — the pair-scoped transfer is deliberately not exposed — so an assistant increases a position's stored margin through the REST endpoint or `@0xmonaco/core`.
## Upgrade
```bash theme={null}
npm install @0xmonaco/core@1.0.41 @0xmonaco/types@1.0.41 @0xmonaco/react@1.0.41 @0xmonaco/contracts@1.0.41
# or bun
bun add @0xmonaco/core@1.0.41 @0xmonaco/types@1.0.41 @0xmonaco/react@1.0.41 @0xmonaco/contracts@1.0.41
```
The MCP server ships at `1.0.41` too; it runs via `npx -y @0xmonaco/mcp-server`, so restarting your assistant picks up the new version, the `clientOrderId` option on the order tools, and the corrected `transfer_collateral_to_risk_bucket` behavior.
Nothing in this release is breaking. The one thing worth acting on: if you followed the 1.0.40 migration and used `transferCollateralToRiskBucket` to *add margin to a position*, switch to `transferCollateralToMarginAccount(parentMarginAccountId, { asset, amount, tradingPairId })` — the risk-bucket transfer funds reusable collateral and no longer ratchets a sole position's stored margin.
# V1.0.42
Source: https://docs.0xmonaco.com/releases/v1.0.42
# Monaco Protocol SDK v1.0.42
This release adds typed WebSocket helpers for the `market_stats` and `account` channels so you no longer hand-roll the raw subscription and key-mapping; lets the candlestick endpoint chart the mark-price series alongside the trade-derived one; and tightens `clientOrderId` so a value can be held by only one of your resting orders at a time. One thing to act on: if you reuse a `clientOrderId` across two of your own resting orders it is now rejected — that rejection identifies the order already holding the handle, but its absence is not evidence that an earlier submit failed, so read the action item before using it to decide whether to retry.
## Added
### Typed `market_stats` and `account` WebSocket helpers
`@0xmonaco/core` gains three typed subscription helpers so the `market_stats` and `account` channels no longer require the raw WebSocket protocol:
* `sdk.ws.marketStats(tradingPairId, handler)` — per-market statistics for one pair (public): index / mark / mid prices, best bid/ask, open interest and its limit, last trade, 24h volume / high / low and price change, and funding state.
* `sdk.ws.marketStatsAll(handler)` — a slower-cadence reduced summary for every market in a single frame (public), each row carrying a `marketStatus` label (`TRADABLE`, `POST_ONLY`, `HALTED`, `NOT_READY`, or `UNKNOWN`).
* `sdk.ws.account(handler)` — the authenticated `account` channel: a margin account's health (equity, collateral, free collateral, margin ratio, maintenance requirement).
`@0xmonaco/types` adds the shapes these deliver — `MarketStatsEvent`, `MarketStatsData`, `MarketStatsAllEvent`, `MarketStatsSummary`, and the `MarketStatus` union — with perp-only fields optional so spot markets flow through the same helpers. The helpers hand you the camelCased ergonomic shape; string decimals stay strings and omitted (perp-only) fields surface as `undefined`. As in 1.0.41, the `account` producer is gated off by default and not yet enabled in the deployed environments, so `sdk.ws.account` subscribes successfully but frames arrive only once the producer is enabled.
### Mark-price candlestick series (`priceType`)
The candlestick endpoint accepts an optional `priceType` (`getCandlesticks`'s `GetCandlesticksParams.priceType` in `@0xmonaco/core` / `@0xmonaco/types`; the `priceType` query parameter on `GET /api/v1/market/pairs/charts/candlestick/{tradingPairId}/{interval}`; `GetCandlesRequest.price_type` on gRPC `MarketService.GetCandles`). `"trade"` (the default) returns the trade-derived OHLCV series, exactly as before. `"mark"` returns the mark-price series for margin pairs: a close-only series where each candle's open, high, low, and close are the minute's closing mark and volume is `0`. Requesting `"mark"` on a spot pair returns a `400`.
## Changed
### `clientOrderId` is unique among your resting orders
A `clientOrderId` may no longer be held by two of your resting orders at once. "Resting" means sitting on the book — an order that never rests never claims the handle. On a single create or replace (`placeLimitOrder` / `placeMarketOrder` / `replaceOrder` in `@0xmonaco/core`; `POST /api/v1/orders`, `PUT /api/v1/orders/{orderId}`; gRPC `CreateOrderRequest` / `ReplaceOrderRequest`; the `place_limit_order` / `place_market_order` / `replace_order` MCP tools), reusing a `clientOrderId` still carried by another of **your own** resting orders is rejected with HTTP `409` / gRPC `ALREADY_EXISTS`, code `CLIENT_ORDER_ID_CONFLICT`; the message names the order already holding it. In a batch (`batchCreate` / `batchReplace`; `POST /api/v1/orders/batch-create` and `.../batch-replace`) the conflict is a **per-item** failure — the request returns `200` / `OK` and only the offending item carries `error.code` `CLIENT_ORDER_ID_CONFLICT` in its result slot — so inspect `results[]` rather than waiting for a transport-level `409`. (A *malformed* handle is not reported the same way on both endpoints: `batchCreate` fails only that item with `INVALID_CLIENT_ORDER_ID`, while `batchReplace` validates every handle up front and rejects the whole request with a `400`.)
**Correction.** This section originally told you to resubmit the request after a lost response and read the `CLIENT_ORDER_ID_CONFLICT` as proof the first attempt landed. That is unsafe and the text below has been corrected. Only a **resting** order holds a `clientOrderId`, so the conflict cannot fire for an attempt that filled on arrival, was `IOC`/`FOK`, or was a `MARKET` order — and a resubmission in those cases is accepted as a new order that **can execute again**, potentially doubling your size. Reconcile against the order WebSocket stream instead; see [Client Order IDs](/developers/trade#client-order-ids).
**Where the conflict fires it is a reliable signal.** If you resubmit with a `clientOrderId` still held by one of your resting orders, `CLIENT_ORDER_ID_CONFLICT` proves that order exists and is still resting — the check runs in the matching engine against live state, not against replicated history, so unlike an order-list lookup it cannot report "not found" for an order that did land. Read the named order rather than placing a second one.
**Its absence proves nothing, so do not resubmit to probe for it.** The handle is claimed only when an order comes to rest and is released the moment it reaches a terminal state, so no conflict is raised when the first attempt filled, was cancelled, was rejected, or never rested at all. A resubmission then places a genuinely new order and can execute a second time. After a lost response, reconcile on the order stream (`sdk.ws.userOrders`), which carries your `clientOrderId` on every event including orders that never rested, and resubmit only once you have positive evidence the intent did not land.
**Scope is per user, among resting orders only.** Two different users may hold the same value at once, and the value frees the moment the order holding it reaches a terminal state (filled, cancelled, expired, rejected), so a stable per-quote label can be reused indefinitely. A replacement may restate the original's handle — the original releases it as the replacement claims it, matching the existing guidance to restate the value on every replace. Orders that carry no `clientOrderId` are unaffected, and orders already resting when this shipped keep whatever handle they carry.
## Upgrade
```bash theme={null}
npm install @0xmonaco/core@1.0.42 @0xmonaco/types@1.0.42 @0xmonaco/react@1.0.42 @0xmonaco/contracts@1.0.42
# or bun
bun add @0xmonaco/core@1.0.42 @0xmonaco/types@1.0.42 @0xmonaco/react@1.0.42 @0xmonaco/contracts@1.0.42
```
The MCP server ships at `1.0.42` too; it runs via `npx -y @0xmonaco/mcp-server`, so restarting your assistant picks up the new version. Nothing in this release is breaking. The one thing worth acting on: if you assign a `clientOrderId` and a single create's response is lost, **do not blind-resubmit** — reconcile on the order stream (`sdk.ws.userOrders`) first. A `409` / `CLIENT_ORDER_ID_CONFLICT` confirms an order is still resting under that handle, but no conflict does not mean nothing landed: an order that filled on arrival or never rested releases the handle, and resubmitting can execute again.
# V1.0.43
Source: https://docs.0xmonaco.com/releases/v1.0.43
# Monaco Protocol SDK v1.0.43
This release adds server-side TWAP parent orders that slice a large order across a time window; in-place partial cancel that shrinks a resting order without losing its queue position; a mark-price candle series on the OHLCV WebSocket channel; a `wrappedNative` balance flag that changes how the vault client deposits the chain's wrapped-native asset; and four spot cost-basis fields on portfolio assets. It also makes a full-close take-profit / stop-loss supersede the position's previous full-close leg of the same type in one atomic step, and fixes the `marginRatio` reported for accounts at zero or negative equity. Two things to act on: if you deposit the wrapped-native asset (e.g. WSEI) it now goes in as native coin with no ERC20 approval — see the vault change below; and `marginRatio` can now come back as a very large sentinel, so clamp it for display.
## Added
### TWAP parent orders
Submit a single **TWAP parent order** and the matching engine slices it into a schedule of market child orders across an execution window, averaging price over time instead of paying market impact in one print. Four authenticated methods on `sdk.trading` (`createTwapOrder(tradingPairId, side, quantity, options)` — `POST /api/v1/orders/twap`, gRPC `OrdersService.CreateTwapOrder`; `getTwapOrder(twapOrderId)` — `GET /api/v1/orders/twap/{twapOrderId}`, gRPC `GetTwapOrder`; `listTwapOrders(params?)` — `GET /api/v1/orders/twap`, gRPC `ListTwapOrders`; `cancelTwapOrder(twapOrderId)` — `POST /api/v1/orders/twap/{twapOrderId}/cancel`, gRPC `CancelTwapOrder`).
The window is exactly one of `durationSeconds` (default bounds 60 s – 30 days) or an `endTime` (RFC 3339 UTC with a `Z` suffix — non-UTC offsets are rejected — optionally with a `startTime`); other options cover `limitPrice`, `slippageToleranceBps` (default 100), `randomize` (default true; seeded ±30% size / ±30% timing jitter), `reduceOnly`, the usual margin context (`tradingMode`, `marginAccountId`, `riskBucketId`, `marginMode`, `leverage`), and a `clientTwapId` (≤36 chars, unique per user + application — a duplicate is rejected with `409`). Both spot and margin markets are supported; delegated-agent sessions are rejected. Live progress streams on the new authenticated `twap_orders` WebSocket channel via `sdk.ws.twapOrders(handler, tradingPairId?)`, delivering a `TwapOrderEvent` with the full parent state on every lifecycle transition. `@0xmonaco/types` adds the full type set (`TwapOrderState`, `CreateTwapOrderOptions`, `CreateTwapOrderResponse`, `CancelTwapOrderResponse`, `TwapOrder`, `ListTwapOrdersParams` / `ListTwapOrdersResponse`, `TwapOrderEvent`) with Zod validation of the window and margin constraints. See [TWAP Orders](/trading-mechanics/twap-orders) for the execution model.
### In-place partial cancel
`sdk.trading.cancelOrder(orderId, { remainingQuantityTarget })` (`POST /api/v1/orders/cancel` `CancelOrderRequest.remainingQuantityTarget`; gRPC `CancelOrderRequest.remaining_quantity_target`) reduces a resting order **in place** to the target remaining quantity, **keeping its queue position** — unlike `replaceOrder`, which retires the order and rests a new one at the back of the book even for a pure size-down. The released delta unlocks exactly as a cancel does (spot unlock / margin reserve). The target means "leave this much resting", so a retried request converges instead of compounding; it must be a positive multiple of the market's quantity step and strictly below the current remaining quantity (emptying an order is a full cancel's job, growing it is a replace). Omit the options argument for the full cancel that existing calls already perform. A new non-terminal `OrderPartiallyCancelled` event announces the reduction on the `orders` WebSocket channel — the order is still resting — carrying the cumulative `filledQuantity`, the reduced `remainingQuantity`, the cancellation `reason` (`null` when none — the cancel request takes no reason input), and `reducedAt`.
### Mark-price candles on the OHLCV WebSocket channel
`sdk.ws.ohlcv(tradingPairId, tradingMode, interval, handler, priceType?)` accepts an optional `priceType` (`"trade"`, `"mark"`, or `"*"`). Omitting it — or passing `"trade"` — subscribes to the trade-derived candles exactly as before; `"mark"` subscribes to the close-only mark-price series for margin pairs (each candle's open = high = low = close = the minute's closing mark, volume `0`), published on `1m`; `"*"` receives both. The argument appends a fifth token to the `ohlcv` channel string, so existing four-token subscriptions are unchanged and never receive the mark series. `@0xmonaco/types` adds the `OHLCVPriceType` union, and `OHLCVEvent` carries an optional `priceType` so a `"*"` subscriber can tell the two streams apart. (The REST candlestick `priceType` shipped in 1.0.42.)
### `wrappedNative` balance flag
`GET /api/v1/accounts/balances` and `GET /api/v1/accounts/balances/{asset_id}` (gRPC `AccountsService.GetBalances` / `GetBalanceByAsset`) now return an optional `wrappedNative` boolean per balance, surfaced as `AccountBalance.wrappedNative` in `@0xmonaco/types`. It is `true` for the chain's wrapped-native asset (e.g. WSEI) and `false` for standard ERC-20 assets. `GET /api/v1/accounts/balances/{asset_id}` returns the flag even when that asset's balance is zero; the list endpoint `GET /api/v1/accounts/balances` omits zero-balance rows. It is the signal the vault client keys native-coin deposits on (see the vault change below).
### Spot cost-basis fields on portfolio assets
`sdk.portfolio.getAssets()` (`GET /api/v1/accounts/me/portfolio/assets`; gRPC `GetPortfolioAssets`) surfaces four optional spot cost-basis fields per asset row — `avgCost`, `unrealizedPnl`, `unrealizedRoe`, and `cumRealized` (all optional decimal strings). They are populated from the spot cost-basis engine when a checkpoint exists for the asset; `unrealizedPnl` and `unrealizedRoe` additionally require a mark price, and `unrealizedRoe` is additionally omitted when the cost basis is zero.
## Changed
### Vault deposits key native-coin handling on `wrappedNative`
The vault client now decides native-coin handling from the balances API's `wrappedNative` flag instead of a zero token address. The exchange's wrapped-native asset (e.g. WSEI) carries a real ERC20 address — the vault wraps native deposits into it, so `address(0)` never appears:
* `sdk.vault.deposit(assetId, amount)` routes a `wrappedNative` asset to the payable `depositNative`, sending `amount` as `msg.value` (the vault wraps on entry) — **no ERC20 approval is required or checked**. Every other asset keeps the approve + `depositERC20` flow, and an absent or `false` flag is treated as a standard ERC-20.
* `sdk.vault.needsApproval(assetId, amount)` returns `false` for a `wrappedNative` asset without reading the on-chain allowance, since `depositNative` consumes none.
* Withdrawals of the wrapped-native asset pay out native coin (the vault unwraps); this is decided server-side and needs no request change.
### Full-close TP/SL supersedes the previous full-close leg
Attaching a full-close (`closePosition: true`) take-profit or stop-loss with `sdk.positions.attachPositionTpSl()` (REST `POST /api/v1/positions/{positionId}/tp-sl`; gRPC `PositionsService.AttachPositionTpSl`) now supersedes that position's previously active full-close legs of the same condition type: the old legs are cancelled atomically in the same engine step that creates the replacement, each announcing a `conditional_order_update` event with `reason: "cancelled"` before the replacement's `"created"`. Moving a TP or SL is therefore one call, with no window in which the position holds both levels or neither — the non-atomic cancel-then-reattach workaround is no longer needed. Three things survive a supersede: the superseded leg's OCO sibling, every fixed-`quantity` leg (laddered partial exits still stack), and legs attached to an entry order rather than to the position (entry-attached legs are excluded in both directions).
## Fixed
### `marginRatio` for accounts at zero or negative equity
`GetPositionRisk` (`GET /api/v1/positions/{positionId}/risk`) and the authenticated `account` WebSocket channel (`AccountEventData.marginRatio`) now report a maintenance-aware `marginRatio` for accounts whose equity is not positive. An account that still carries an open maintenance requirement at zero or negative equity now reports the maximum-distress ratio instead of `0` (equity exactly zero) or a negative ratio (equity gone negative from bad debt) that previously sorted as *safer* than a healthy account. Accounts with no open positions continue to report `0`. The maximum-distress value is a very large decimal string (`Decimal::MAX`, \~7.9e28); treat a ratio at or above the liquidation threshold as maximal distress and clamp it for display.
## Upgrade
```bash theme={null}
npm install @0xmonaco/core@1.0.43 @0xmonaco/types@1.0.43 @0xmonaco/react@1.0.43 @0xmonaco/contracts@1.0.43
# or bun
bun add @0xmonaco/core@1.0.43 @0xmonaco/types@1.0.43 @0xmonaco/react@1.0.43 @0xmonaco/contracts@1.0.43
```
The MCP server ships at `1.0.43` too; it runs via `npx -y @0xmonaco/mcp-server`, so restarting your assistant picks up the new version. Nothing in this release is breaking. Two things worth acting on: if you deposit the wrapped-native asset (e.g. WSEI), drop the ERC20 approval step — `deposit` now sends it as native coin through the vault's payable `depositNative`, and `needsApproval` returns `false` for it; and if you read `marginRatio`, treat a value at or above the liquidation threshold as maximal distress and clamp the `Decimal::MAX` sentinel for display.
# V1.0.44
Source: https://docs.0xmonaco.com/releases/v1.0.44
# Monaco Protocol SDK v1.0.44
This release adds an opt-in `asErc20` deposit path for the chain's wrapped-native asset, so you can deposit it as its ERC20 (approve + `depositERC20`) instead of the native-coin default introduced in 1.0.43; and a `withdrawableCollateral` figure on portfolio margin that names the amount that can actually leave a margin account or risk bucket. It also documents a server-side correction to `replaceOrder`: `newOrder.quantity` now names the order's new **total** quantity, and the replacement inherits the original's fill history. One thing to act on: if you replace a partially filled order by `quantity`, pass the new total rather than the remainder — see the change below.
## Added
### `asErc20` deposit option for the wrapped-native asset
`sdk.vault.deposit()` accepts an `options.asErc20` flag (`@0xmonaco/core`; `DepositOptions` and `DepositSchema` in `@0xmonaco/types`). Since 1.0.43 the chain's wrapped-native asset (e.g. WSEI) deposits as native coin through the payable `depositNative` with no ERC20 approval. Passing `asErc20: true` instead deposits it as its ERC20 through the standard approve + `depositERC20` flow — for wallets that hold the wrapped ERC20 itself — and `sdk.vault.needsApproval()` with the same option reads the real WSEI allowance rather than short-circuiting to `false`. Every other asset, and a wrapped-native deposit without the flag, is unchanged.
`options` is the fifth argument to `deposit()` and the third to `needsApproval()`. The ERC20 path is not auto-approved — `deposit()` throws a `ContractError` (`code === "CONTRACT_ERROR"`) whose `revertReason` is `"APPROVAL_REQUIRED"` if the allowance is short, so approve first exactly as you would for any ERC20:
```ts theme={null}
// deposit the wrapped-native asset as its ERC20 (approve + depositERC20)
if (await sdk.vault.needsApproval(assetId, amount, { asErc20: true })) {
await sdk.vault.approve(assetId, amount);
}
await sdk.vault.deposit(assetId, amount, /* autoWait */ true, /* target */ "spot", { asErc20: true });
```
### `withdrawableCollateral` on portfolio margin
`sdk.portfolio.getMargin()` (`GET /api/v1/accounts/me/portfolio/margin`; gRPC `AccountsService.GetPortfolioMargin`) adds `withdrawableCollateral` (decimal string) to both `PortfolioMarginTotal` and each `PortfolioRiskBucket` in `@0xmonaco/types`. It is the largest amount that can actually leave the parent margin account or risk bucket — the bound the engine enforces on transfers-out and withdrawals, and the effective ceiling on funding new isolated / first-use positions (bucket allocation passes the same principal-gated door). Unlike `parentAvailableCollateral` / `freeCollateral` (equity figures) it excludes unrealized gains and non-principal balances such as banked realized PnL; on the parent it is `min(deposited principal − bucket allocations, freeCollateral − max(unrealizedPnl, 0))`, floored at zero. Size transfers, withdrawals, and new isolated positions from `withdrawableCollateral`; keep `freeCollateral` as trading-margin headroom for existing cross exposure. The `GetAvailableCollateralResponse.marginAvailableCollateral` doc was clarified to say the same.
## Changed
### `replaceOrder`'s quantity is the order's new total
`sdk.trading.replaceOrder()` (`PUT /api/v1/orders/{orderId}`; gRPC `OrdersService.ReplaceOrder`) now treats `newOrder.quantity` as the order's new **total** quantity. On a partially filled order the replacement rests `total - filled` and locks collateral for that remainder only (FIX cancel/replace convention: `LeavesQty = OrderQty - CumQty`); the total must exceed the filled amount. Previously the value was placed as a fresh order size, so replacing a 96/100-filled order with `quantity: "200"` rested 200 and locked for all 200 (total exposure 296) — it now rests 104 and reads as 96/200 filled. A replace that omits `quantity` keeps the order's total, derived against the live fill state inside the matching step, so a fill landing while the request is in flight can no longer make the replacement rest more than the true remainder; the response's `updatedFields.quantity` reports that total (previously a point-in-time remainder snapshot). Unfilled orders are unaffected, and margin reduce-only replacements are exempt (explicit quantities stay close sizes). `batchReplace` items (`BatchReplaceOrderItem.quantity`) carry the same total semantics.
The replacement also inherits the original's fill history — filled quantity, VWAP, and realized fee / payment aggregates carry onto the new order's row, exactly as if the order had been edited in place. The retired original keeps its own terminal row, so summing fill aggregates **across** a replacement chain's rows double-counts the carried history; per-order reads and trade-level data are unaffected. Types and wire shape are unchanged — this is a server-side semantics correction, so clients on any SDK version get the new behavior.
### Package metadata points at the public docs site
Every published package drops its `repository` field and sets `homepage` to the public docs. The previous `repository` URL referenced a private GitHub repository, so the Repository and Homepage links on the package page returned 404 for anyone outside the org; there is no public source repository to point at, so the field is omitted rather than redirected. The five npm packages (`@0xmonaco/core`, `@0xmonaco/types`, `@0xmonaco/react`, `@0xmonaco/contracts`, `@0xmonaco/mcp-server`) point at `https://docs.0xmonaco.com/sdk/typescript`; the two published Rust crates point at their own reference — `monaco-sdk` at `https://docs.0xmonaco.com/sdk/rust-rest` and `monaco-grpc-sdk` at `https://docs.0xmonaco.com/sdk/rust-grpc` (their rustdoc landing link follows the same target, and `documentation` still points at docs.rs). No code, type, or behavior changes — this is why `@0xmonaco/react`, `@0xmonaco/contracts`, `@0xmonaco/mcp-server`, and both Rust crates bump to 1.0.44 with no behavior change.
## Upgrade
```bash theme={null}
npm install @0xmonaco/core@1.0.44 @0xmonaco/types@1.0.44 @0xmonaco/react@1.0.44 @0xmonaco/contracts@1.0.44
# or bun
bun add @0xmonaco/core@1.0.44 @0xmonaco/types@1.0.44 @0xmonaco/react@1.0.44 @0xmonaco/contracts@1.0.44
```
The MCP server ships at `1.0.44` too; it runs via `npx -y @0xmonaco/mcp-server`, so restarting your assistant picks up the new version. The API changes are additive and backward-compatible — a new optional `withdrawableCollateral` response field on portfolio margin and the new opt-in `DepositOptions` / `DepositSchema` types — and every SDK bump is a patch release, so the upgrade is drop-in for almost everyone. The one compatibility boundary is behavioral: `replaceOrder`'s `quantity` is now the order's **total**, so a partially filled `replaceOrder` that used to pass the remaining quantity now rests the wrong size — and is rejected outright when that value is at or below the amount already filled. Audit those calls before upgrading; everything else is backward-compatible.
# V1.0.45
Source: https://docs.0xmonaco.com/releases/v1.0.45
# Monaco Protocol SDK v1.0.45
This release adds a public `instrument` WebSocket channel that streams trading-pair lifecycle and configuration changes — new listings, delistings, and edits to tick size, step size, order-size bounds, or category — with each frame carrying the market's full current configuration, perp leverage and margin-rate bounds included. Positions gain a `cumFees` field carrying the position's whole-life trading fees (opening fills included). Two separate fixes also land: closed-position `netRealizedPnl` / `realizedRoe` now net opening-fill fees, and faucet history returns one entry per on-chain deposit. One thing to act on: closed-position `netRealizedPnl` and `realizedRoe` now fold in opening-fill fees, so those figures shift — usually down, but up where the opening fills earned a maker rebate (fees are signed) — read the fix below before you rely on them.
## Added
### Public `instrument` WebSocket channel
A new public, unauthenticated channel streams trading-pair lifecycle and configuration changes: new listings, delistings, and non-activation edits to tick size, quantity step, order-size bounds, or category. Each frame carries the market's full current configuration — including, for perp markets, leverage and margin-rate bounds — but those margin parameters are payload only: they are read into every frame, and an edit to them alone does not itself emit one. Subscribe to `instrument:` for one market, or the bare `instrument` channel (`instrument:*`) for every market; a subscribe-time snapshot of the current active-pair configuration precedes the live diffs.
Each live `Event` frame carries one market's **full current configuration**, not just the changed fields, so consume it as a per-market terminal-state snapshot rather than an additive delta — `changedFields` names which fields the mutation touched, for callers that want to react selectively. The subscribe-time baseline arrives as a single `Snapshot` frame whose `data` is an array of those same per-market config objects (each a `listing`). The lag policy is conflate: a subscriber that falls behind is fast-forwarded to the latest frame rather than disconnected. `@0xmonaco/types` ships the `InstrumentEvent`, `InstrumentEventData`, and `InstrumentChangeKind` types; there is no typed `sdk.ws` helper yet, so subscribe over the raw WebSocket protocol and map the snake\_case wire keys onto `InstrumentEventData`. See [WebSockets](/reference/websockets#instrument).
### `cumFees` on positions
`sdk.positions.listPositions()` and `sdk.positions.getPosition()` (`GET /api/v1/positions`, `GET /api/v1/positions/{positionId}`; gRPC `PositionsService.ListPositions` / `GetPosition`) add `cumFees` (decimal string, signed) to `Position` in `@0xmonaco/types`. It is the position's **total lifetime trading fees** — opening-fill fees plus every reducing execution (decreases, close, and any liquidation penalty) — negative when net rebated, and inclusive of the application-fee component. Unlike `feesPaid`, which is only the closing-side reducing-execution portion and omits opening fills, `cumFees` is the complete figure, present on open and terminal rows alike. It is `null` when the whole-life total cannot be completed — before the position's first PnL sample, after its samples age out of PnL retention, for positions predating the PnL service, or when a legacy reducing row carries a null fee. Unlike `feesPaid`, `cumFees` is not gated on the reducing-history completeness check, so in the rare case where a reducing-history row is entirely absent (a legacy data gap) it can be non-null and understated.
## Fixed
### Closed-position realized PnL and ROE net opening-fill fees
`netRealizedPnl` and `realizedRoe` on terminal positions (`GET /api/v1/positions`, `GET /api/v1/positions/{positionId}`; gRPC `PositionsService.ListPositions` / `GetPosition`) now subtract the position's opening-fill fees as well, not only its closing-side reducing-execution fees. A closed position's entry fees were previously ignored, mis-stating net realized PnL and the realized return; terminal rows now read net of the full fee life **whenever the opening PnL sample is available**. Opening-fill fees are signed, so the figure usually drops, but rises where those fills earned a maker rebate. Rows whose sample has aged out of PnL retention, or that predate the PnL service, keep the previous closing-execution net (opening fees not folded in) — `cumFees` is `null` for those — so not every historical row is fully fee-netted. `realizedRoe` follows `netRealizedPnl`. If you render realized PnL or ROE on closed positions, expect the values to shift.
### Faucet history returns one entry per on-chain deposit
Faucet history (the `recentRequests` list on faucet info; `GET /api/v1/faucet/info`) now returns one `minted` entry per on-chain deposit: the 1000 USDC spot mint and the 1000 USDC margin mint appear as separate entries, each with its own `amount` and — for a confirmed leg — its own `txHash`, and the margin leg labelled `USDC (margin)` — mirroring the mint response. A leg submitted but not yet confirmed carries no hash. A session was previously squashed into a single 2000 entry that carried only the spot transaction hash.
## Upgrade
```bash theme={null}
npm install @0xmonaco/core@1.0.45 @0xmonaco/types@1.0.45 @0xmonaco/react@1.0.45 @0xmonaco/contracts@1.0.45
# or bun
bun add @0xmonaco/core@1.0.45 @0xmonaco/types@1.0.45 @0xmonaco/react@1.0.45 @0xmonaco/contracts@1.0.45
```
The MCP server ships at `1.0.45` too; it runs via `npx -y @0xmonaco/mcp-server`, so restarting your assistant picks up the new version. Every SDK bump is a patch release. The new surfaces are additive — a `cumFees` field on positions and the new raw `instrument` WebSocket channel — so existing request and wire shapes are unchanged and the upgrade is drop-in; adopt the new field and channel only if you want them (a raw `instrument` client must handle the `Snapshot` baseline frame as well as `Event` diffs — see [WebSockets](/reference/websockets#instrument)). The one behavioral change to check is closed-position `netRealizedPnl` and `realizedRoe`, which now net opening-fill fees when the opening PnL sample is available, so stored or displayed realized-PnL figures for closed positions shift — usually lower, but higher where the opening fills earned a maker rebate.
# V1.0.46
Source: https://docs.0xmonaco.com/releases/v1.0.46
# Monaco Protocol SDK v1.0.46
This release adds two things: a post-execution transaction cost analysis (TCA) report for TWAP parent orders — realized average price scored against the arrival price and a window-TWAP benchmark, with signed basis points for both — and a typed `sdk.ws.instruments()` subscription for the public `instrument` WebSocket channel that shipped server-side in v1.0.45, which until now was reachable only over the raw WebSocket protocol. Both surfaces are additive; nothing existing changes shape, so the upgrade is drop-in with no action required.
## Added
### TWAP transaction cost analysis (`getTwapOrderTca`)
`sdk.trading.getTwapOrderTca(twapOrderId)` (`GET /api/v1/orders/twap/{twapOrderId}/tca`; gRPC `OrdersService.GetTwapOrderTca`) returns a post-execution TCA report for a **terminal** TWAP parent — available once the parent is `COMPLETED` or `CANCELLED`; a non-terminal parent returns `400`. The report scores the parent's realized average price against two references over its **active window** (`windowStartAt` to `windowTerminalAt` — the actual start-to-terminal span, never the planned end): the **arrival price** (the reference series' latest minute bucket closing at or before the window start) and the **window-TWAP benchmark** (the average reference price over the window's sealed minute buckets). `benchmarkSource` names the one series both references came from — `risk_mark_1m` for marked pairs, with `ohlcv_1m_close` as the spot fallback. The two derived scores, `slippageVsArrivalBps` and `performanceVsBenchmarkBps`, are signed basis points at 4 decimal places, and **positive always means the execution beat the reference** — a BUY filled below it, a SELL above it.
Two things to read carefully:
* `realizedAvgPrice` is `executedNotional / executedQuantity`, **gross of fees** — the price the fills traded at, deliberately excluding taker fees and maker rebates so it compares cleanly against price benchmarks.
* The six analytics fields (`realizedAvgPrice`, `arrivalPrice`, `benchmarkTwapPrice`, `benchmarkSource`, and both bps fields) are **optional and omitted from the response body — the key is absent, not sent as `null` — whenever that field cannot be computed honestly**, and each has its own condition. `realizedAvgPrice` comes straight from the fills and is absent only when nothing executed. The three reference fields are absent when no reference series qualified: an arrival bucket staler than 5 minutes is rejected, the benchmark requires at least 90% coverage of the window's sealed minute buckets, and a window too short to seal a single bucket is not scoreable. Each bps field is absent when either of its inputs is — so a parent that filled but had no qualifying reference series still reports `realizedAvgPrice`, just no scores. The structural fields (`twapOrderId`, `tradingPairId`, `state`, `side`, `totalQuantity`, `executedQuantity`, `shortfallQuantity`, and the window bounds) are always present, so a report is self-describing and closes the fill-rate arithmetic without a second lookup.
Owner scoping matches the rest of the TWAP surface: another user's parent returns a `404` byte-identical to an unknown ID, and delegated-agent sessions are rejected. `@0xmonaco/types` ships the `GetTwapOrderTcaResponse` interface. See [Trading](/sdk/typescript/trades#twap-orders) and [TWAP Orders](/trading-mechanics/twap-orders).
### Typed `sdk.ws.instruments()` WebSocket subscription
`sdk.ws.instruments(handler, tradingPairId?)` is a typed subscription for the public `instrument` channel: market lifecycle and configuration changes — listings, delistings, and edits to tick size, quantity step, order-size bounds, or category. The perp-only leverage/margin-rate bounds are carried in every frame's full config but are payload only: an edit to those margin parameters alone does not emit a frame. Omit `tradingPairId` for all markets or pass one to filter. Handlers receive the typed `InstrumentEvent` from `@0xmonaco/types`; every frame carries the market's **full current configuration** as a terminal-state snapshot — never an additive delta — with `changedFields` naming what the mutation touched, and the subscribe-time snapshot of the active-pair list is fanned out to the handler as one event per market before the live diffs. A subscriber joining a channel the client already holds is replayed the current baseline from a local cache (the server snapshots only genuinely new wire subscriptions), so every subscriber gets the baseline regardless of ordering. The channel itself shipped server-side in v1.0.45 and was previously reachable only over a raw WebSocket connection with hand-mapped snake\_case keys. See [WebSockets](/sdk/typescript/websockets#instruments-channel).
## Upgrade
```bash theme={null}
npm install @0xmonaco/core@1.0.46 @0xmonaco/types@1.0.46 @0xmonaco/react@1.0.46 @0xmonaco/contracts@1.0.46
# or bun
bun add @0xmonaco/core@1.0.46 @0xmonaco/types@1.0.46 @0xmonaco/react@1.0.46 @0xmonaco/contracts@1.0.46
```
The MCP server ships at `1.0.46` too; it runs via `npx -y @0xmonaco/mcp-server`, so restarting your assistant picks up the new version. Every SDK bump is a patch release. Both new surfaces are additive — a new TCA endpoint and a typed helper for an existing channel — so existing request and wire shapes are unchanged and the upgrade is drop-in; adopt them only if you want them. When you do read a TCA report, handle the absent-key case for the six analytics fields — a report with only the structural fields is a valid answer, not an error.
# V1.0.47
Source: https://docs.0xmonaco.com/releases/v1.0.47
# Monaco Protocol SDK v1.0.47
This release is mostly TWAP. Parents gain a second execution style — `PASSIVE`, which rests each slice as a post-only maker quote at the touch and takes only what the window forces — and a conditional mode that keeps a parent dormant until the market reaches a price you name. Alongside those, a `postOnly` batch-replace item that would cross the book no longer costs you the original order, funding payments now appear in the movements feed, and the `account` WebSocket channel carries a tie-breaking `sequence`. Two things deserve reading before you upgrade: the corrected `clientOrderId` guidance, which previously told you to do something unsafe, and the exact scope of the batch-replace change.
## Added
### `PASSIVE` TWAP execution style
`sdk.trading.createTwapOrder()` accepts an optional `executionStyle` of `"TAKER"` or `"PASSIVE"`. `TAKER` is the default and is the existing v1 behavior, unchanged — banded `MARKET` slices fired at each slot.
`PASSIVE` works each slice as a maker instead. The engine rests a post-only `LIMIT` `GTC` child at the same-side touch, chases the touch with throttled cancel/replace reprices, and at the slot deadline sweeps whatever the child did not earn as a banded `MARKET` child. The result is maker where the book allows it and taker only for what the window forces. Two bounds hold while chasing: the parent's own `limitPrice`, which is never crossed, and the parent's `slippageToleranceBps` band around the **protective reference price** — the oracle mark on a margin market when one is available, otherwise a robust book-derived venue price (from the best bid/ask, when the spread is not degenerate) falling back to the last trade. Spot parents always use that book-derived chain, since spot has no oracle mark. There is one exception worth knowing if you lean on the band for protection: when the engine can resolve **no** reference at all — a one-sided book on a pair that has not traded in this process, so no mark, no trustworthy two-sided venue price and no last trade — the band check is skipped rather than failing closed, and `limitPrice` becomes the only bound on the resting quote. Set one on a passive parent if you need a hard ceiling in a market that thin. A passive placement whose target price would cross executes immediately as a banded taker instead — the book is already offering at or better than the passive target, so resting would be pointless.
Passive parents plan on their own cadence: a **60-second** target slice interval by default, against the taker default of \~5 seconds, because a child has to rest long enough to earn queue position. The 2,880-slice cap is the same for both styles, so a long window stretches its interval rather than adding slices. Both the cadence and the reprice throttle (1 second minimum between attempts) are operator-tunable.
Cancelling a passive parent is **cancel-only and atomic**: a resting child is removed in the same step that terminalizes the parent, fills it already earned stand, and nothing is left on the book. There is no final sweep on a user cancel.
Fees follow each fill's own side. Passive maker fills carry the pair's maker fee and **no application fee** — the application fee is a taker-side fee — while the deadline sweeps and any crossing take pay application taker fees as usual.
`executionStyle` is on the TWAP read model from `getTwapOrder()` / `listTwapOrders()`, and the `twap_orders` WebSocket channel gains three reasons: `passive_child_placed` (a post-only child now rests at the touch), `child_repriced` (the reprice step ran), and `passive_fill` (maker fills on the resting child folded into the parent). Deadline sweeps and crossing takes still surface as `slice_placed`.
Two of those three carry a case worth reading before you build on them. `child_repriced` is also what the cancel-only fallback emits when the old child was cancelled but the repost did not happen — so it does not imply a child is resting. The channel gives you no way to tell the two apart either: `TwapOrderEventData` carries no active-child field, so resting-child status is simply not observable from `twap_orders`. Track the child through the ordinary order channels if you need it. And the maker fill that *completes* the parent reports `completed`, not `passive_fill`, so a client waiting on a final `passive_fill` waits forever. See [TWAP Orders](/trading-mechanics/twap-orders#passive-execution) and [Trading](/sdk/typescript/trades#twap-orders).
### Conditional (price-triggered) TWAP parents
A TWAP parent can now wait for a price instead of only for a clock. Pass `triggerPrice` together with an explicit `triggerDirection` (`"ABOVE"` or `"BELOW"`) — both or neither — and the parent is created `PENDING`, watching rather than executing, and activates when the reference price crosses your level. `triggeredAt` records when it fired, and activation is one-way: once trading, the trigger is never re-evaluated.
The window floats. A conditional parent takes `durationSeconds` only — `startTime` and `endTime` are rejected — because its window is `[trigger instant, trigger instant + durationSeconds]` and cannot be known at creation. Read the armed window back off the parent rather than scheduling on the create response, whose `firstSliceAt` and `endTime` are provisional placeholders until the trigger fires.
Optional `conditionalExpiry` bounds the wait: an RFC 3339 UTC deadline (`Z` suffix only) that is valid only alongside a trigger, must be in the future, and both defaults to and is capped at 30 days from creation. If the trigger has not fired by then the parent goes `CANCELLED` having executed nothing, with the terminal reason `trigger expired`. It fires on the clock alone, so a halted or unpriceable market cannot leave a parent waiting past its deadline.
Two refusals to know about. A trigger **already satisfied** at creation returns `400` rather than starting immediately — omit `triggerPrice` if immediate execution is what you want. And on a margin market the reference is the oracle mark, so a create is rejected `400` while the market has no mark yet; an already-waiting parent keeps evaluating against the venue reference price if the mark later goes away.
`triggerPrice`, `triggerDirection`, `conditionalExpiry`, and `triggeredAt` are all on the read model and on the `twap_orders` WebSocket payload, and the channel gains the `triggered` and `trigger_expired` reasons. See [TWAP Orders](/trading-mechanics/twap-orders#conditional-twap-price-triggered-activation).
### `sequence` on the `account` WebSocket channel
Frames on the authenticated `account` channel now carry `sequence`, a monotonic per-margin-account publish counter within one producer run. It is a tie-breaker, not a stream cursor: dedupe by `updatedAt` with newest winning, and reach for `sequence` only to order two frames carrying the same `updatedAt`. It may skip values, and it resets to zero when the producer restarts — which can happen while your socket stays connected — so it is never a gap detector and never a cross-restart ordering mechanism. Every frame still carries the account's full current health, so a skipped value costs you nothing. Note that the producer emitting these frames is still gated off by default and not enabled in the deployed environments, so this is a contract addition to build against rather than a live stream change. See [WebSockets](/sdk/typescript/websockets#account-channel).
## Changed
### A crossing `postOnly` batch-replace item no longer costs you the original
`batchReplace` used to run its cancel pass first, so a `postOnly` replacement that would cross the book was refused *and* left you without the order it was replacing. The crossing check now runs in the pre-cancel preflight, so a refused item normally leaves its original resting — same order ID, same queue position. The item still reports `error.code === "POST_ONLY_WOULD_CROSS"`; only the fate of the original changed. This brings `batchReplace` into line with single `replaceOrder`, which already validated before cancelling. The check is evaluated against the book as it will stand once the batch's own cancels are applied, so a two-sided ladder shifting across its own resting prices is unaffected.
It is not a guarantee, because the preflight cannot see everything. Three narrow cases still report `POST_ONLY_WOULD_CROSS` with the original cancelled: your item crosses a replacement that an earlier item in the same batch just rested; it crosses another item's original that failed to cancel and stayed on the book; or you listed the same original twice with different replacements, where a refused occurrence's sibling still cancels the shared original. A fourth goes the other way — an earlier non-`postOnly` item that would have taken the touch is not visible either, so a quote that would have rested is refused and its original simply stays put. Confirm against your open orders rather than treating the error code alone as a guarantee.
Unchanged: every other per-item placement failure — insufficient balance, a self-trade against an order outside the batch, a risk rejection — still leaves the original cancelled. See [Batch Replace](/developers/trade#batch-replace).
### Funding payments appear in the movements feed
`GET /api/v1/accounts/movements` (`sdk.profile.getPaginatedUserMovements()`) now includes funding payments as `funding` transactions, unioned in at read time from the funding ledger. Filter to them with `transactionType=FUNDING`: the SDK method validates the value against an upper-case-only Zod enum and throws client-side on a lower-case one, while raw REST also accepts `funding` since the server matches case-insensitively. The `amount` is a magnitude, with `entryType` carrying the direction: `DEBIT` when the account paid funding, `CREDIT` when it received it. Like every other REST movement row, both enum values come back upper-case.
A funding movement is a collateral USD delta rather than a token transfer, so it reports `decimals` of `0` and an `amountRaw` equal to its `amount`. It carries the same `id` as its live `movements` WebSocket frame, so a client consuming both feeds deduplicates by `id` — but the two payloads are **not** identical, so do not diff them field for field. The REST row keeps the upper-case enum values and resolves `assetId`, `symbol`, and `decimals`; the WebSocket frame uses the lower-case forms and carries no resolved asset id. Zero-amount funding windows are excluded, and funding is scoped to the requesting application like every other movement. See [Account Balances](/developers/account-balances#movements).
### `MonacoWebSocket` and `MessageHandler` now live in `@0xmonaco/types`
Both are re-exported from `@0xmonaco/core` rather than defined there, making `@0xmonaco/types` the single source of truth for the channel surface. The public surface is unchanged and existing imports from `@0xmonaco/core` keep working.
## Fixed
### `MonacoSDK.ws` types the full channel surface
`createMonacoSDK()` instances carried a stale inline copy of the WebSocket type, so `positions`, `liquidations`, `account`, `instruments`, `marketStats`, and `marketStatsAll` were invisible on `sdk.ws` even though the runtime had them, and `ws.ohlcv` did not accept its optional `priceType` argument. `MonacoSDK.ws` is now typed as the full `MonacoWebSocket` interface, so all six channels and the `priceType` argument type-check. Runtime behavior is unchanged — this only unblocks calls TypeScript was rejecting.
### Corrected `clientOrderId` guidance — never retry on a lost response
The `clientOrderId` documentation was wrong in two ways, one of them dangerous. It described the handle as "not de-duplicated and not unique", and it told callers to reconcile a lost response against order history before resubmitting.
Uniqueness **is** enforced, but only among your orders that are **resting on the book**. An order that never rests — `MARKET`, `IOC`/`FOK`, or a `LIMIT` that fills completely on arrival — never claims the handle, so no conflict can be raised for it and a resubmission is accepted as a new order that can execute again. The handle is also released on any terminal state. It is a correlation handle, not an idempotency key, and it cannot make a lost-response retry safe.
Reconciliation belongs on the order WebSocket stream, not on order history. The stream carries the handle even for orders that never rested, and a matching event there is positive proof the order landed — while silence is never evidence that it did not. Order history is replica-backed and can omit an order that did land, and it never contains an order rejected before acceptance at all.
The same correction landed on the MCP `place_limit_order`, `place_market_order`, and `replace_order` tool descriptions. MCP exposes no order-stream tool, so an agent cannot resolve an ambiguous outcome from inside MCP: report it rather than retrying. No wire, schema, or behavior change — descriptions and documentation only.
## Upgrade
```bash theme={null}
npm install @0xmonaco/core@1.0.47 @0xmonaco/types@1.0.47 @0xmonaco/react@1.0.47 @0xmonaco/contracts@1.0.47
# or bun
bun add @0xmonaco/core@1.0.47 @0xmonaco/types@1.0.47 @0xmonaco/react@1.0.47 @0xmonaco/contracts@1.0.47
```
The MCP server ships at `1.0.47` too; it runs via `npx -y @0xmonaco/mcp-server`, so restarting your assistant picks up the new version. Every SDK bump is a patch release.
Nothing here is breaking: the TWAP additions are optional fields on an existing create, `sequence` and the `funding` movements are additive, and the batch-replace change only makes a rejection less costly. Two action items, though. First, if any code path retries an order after a lost or ambiguous response, keyed on `clientOrderId` for safety, change it — the handle does not protect a `MARKET`, `IOC`/`FOK`, or fully-filling `LIMIT` order, and a retry can execute twice. Reconcile on the order stream instead. Second, if you handle `POST_ONLY_WOULD_CROSS` in a batch-replace ladder by re-placing the original from scratch, that will now double it in the common case: re-price rather than re-place, and confirm against your open orders for the narrow cases that still cancel.
# V1.0.48
Source: https://docs.0xmonaco.com/releases/v1.0.48
# Monaco Protocol SDK v1.0.48
A small release. Risk-bucket rows across the margin and portfolio surfaces gain `availableOrderCollateral` — the risk engine's own answer to "how much more can this bucket commit to new orders", which is a different figure from `withdrawableCollateral` and deliberately so. React gains `useInstruments`, a typed hook over the public `instrument` channel. And the `conditionalOrders` WebSocket parser stops throwing away whole frames it does not fully recognize, which comes with one type-level breaking change: `ConditionalOrderEventData` now declares what the server can actually send, so an exhaustive `switch` over `timeInForce` or `reason` needs a `default` branch to keep compiling.
## Breaking
### `ConditionalOrderEventData` declares the values the server can really send
Two fields widen. `timeInForce` goes from `"GTC" | "IOC"` to the full `TimeInForce` union: the creation paths validate the narrow pair, but the field mirrors an unconstrained server column that is mapped exhaustively over every variant, so a value written by any other path reaches the wire as-is. And `reason` goes from the `ConditionalOrderEventReason` union to `string`, because the wire field is a free-form server string rather than a closed enum — the subscribe-time snapshot already reuses the same shape with `"snapshot"`.
`ConditionalOrderEventReason` is still exported and still documents the canonical live values (`created`, `activated`, `cancelled`, `parent_cancelled`, `triggered`, `failed`, `oco_cancelled`); it is now a narrowing aid rather than a closed set.
Code that switched exhaustively over `timeInForce` or `reason` needs a `default` branch. More generally, every value union on this interface is now documented as "the set the server can send today", not a runtime guarantee — see the Fixed section for why. See [Conditional Orders Channel](/sdk/typescript/websockets#conditional-orders-channel).
## Added
### `availableOrderCollateral` on risk-bucket rows
Risk-bucket rows returned by `getMarginAccountSummary`, `getParentMarginAccountSummary`, and `listMarginAccounts` (`GET /api/v1/margin/accounts/{margin_account_id}`, `GET /api/v1/margin/parent-margin-account`, `GET /api/v1/margin/accounts`; gRPC `MarginAccountsService`) now carry an optional `availableOrderCollateral`, and so do the `riskBuckets` rows from `sdk.portfolio.getMargin()` (`GET /api/v1/accounts/me/portfolio/margin`; gRPC `AccountsService.GetPortfolioMargin`).
It is the collateral still available to open **new** orders in that bucket: the largest increase in the bucket's worst-case order reserve that the risk engine's admission gate will accept, with automatic top-up from the parent margin account's unallocated capacity already included. It is zero while the account or bucket is frozen for liquidation, and for an isolated bucket whose market is halted — a cross bucket picks its market per order, so a halt cannot be folded into its figure, and a halted pair still refuses orders.
This is the trading-side sibling of `withdrawableCollateral`, and the two differ on purpose: this one credits an unrealized gain in full, because a gain is legitimate initial margin for a new position, while a withdrawal never counts one. It already contains the bucket's live free collateral, so do not add `freeCollateral` or `unrealizedPnl` on top; it can also legitimately differ from the same row's `freeCollateral`, which is a persisted reconstruction that cannot see the engine's worst-case resting-order reserve.
Two things to get right before you build on it. It is **not additive across rows** — every bucket's figure includes the same parent unallocated headroom, so summing them multiply-counts it, and committing collateral in one bucket shrinks its siblings' figures. And it is a **requirement budget, not an order size**: an order is charged the increase it causes in the bucket's worst-case per-pair reserve, so a reducing, reduce-only, or opposite-side order sheltered by a larger resting ladder needs less of it or none at all — and can be admitted even when this field reads zero — while an order crossing a losing position can need more than its own margin plus fees. Divide by a market's initial margin rate for indicative notional capacity; the authoritative per-order answer is still `simulateRiskBucketOrderRisk`.
The field is present only on risk-bucket rows served from a live risk engine snapshot. It is absent — the key is omitted, never null — on parent rows and on persisted-fallback reads, where neither term of the gate is reconstructable, so handle `undefined`. On the portfolio surface it is truncated toward zero at quote decimals, like the `withdrawableCollateral` beside it. See [Margin Accounts](/sdk/typescript/margin-accounts) and [Perps Collateral](/developers/perps-collateral).
### `useInstruments` React hook
`useInstruments(tradingPairId?)` is a typed hook over the public `instrument` WebSocket channel, subscribing through `sdk.ws.instruments()` and unsubscribing on unmount. It applies the subscribe-time snapshot and every subsequent change frame as a terminal-state entry per market. Three `change` kinds are emitted today — `listing`, `delisting`, and `config_change`; `halt` and `unhalt` exist on `InstrumentChangeKind` for a future market-regime producer and are not emitted by the admin trading-pair path.
It returns `instruments` — an `InstrumentEventData[]` ordered by symbol and then by trading pair id, so a spot and a perp market sharing a symbol keep a stable order — plus `instrumentsByPair` keyed by that id, `subscribed`, `error`, and `clearError`. Omit the argument to follow every market. A delisted market stays in the list carrying `isActive: false` rather than disappearing, so a consumer can tell "no longer tradable" from "never seen"; filter on `isActive` for the tradable set.
`subscribed` means "registered", not "receiving" — the client accepts subscriptions while the socket is down and sends them on the next open — and `error` is a bad-usage signal (an empty `tradingPairId` is the reachable case), not a transport one. See [`useInstruments`](/sdk/react/hooks/use-instruments).
## Fixed
### The `conditionalOrders` parser no longer drops whole frames on an unexpected value
An unexpected enum value anywhere in a `conditional_order_update` frame used to throw inside the WebSocket message handler, losing the entire update silently. A `timeInForce` of `FOK` or `GTD`, or any `reason` the server added, was enough to do it.
`orderType`, `timeInForce`, `state`, `side`, `positionSide`, `conditionType`, `triggerSource`, `associationType`, and `reason` are now read leniently and passed through as-is, matching what the position, liquidation, and market-stats parsers already do — a dropped frame is a worse failure than an unhandled value. `event_type` stays strict, so a mis-routed frame is still rejected. Match on these fields with a `default` branch. See [Conditional Orders Channel](/sdk/typescript/websockets#conditional-orders-channel).
## Upgrade
```bash theme={null}
npm install @0xmonaco/core@1.0.48 @0xmonaco/types@1.0.48 @0xmonaco/react@1.0.48 @0xmonaco/contracts@1.0.48
# or bun
bun add @0xmonaco/core@1.0.48 @0xmonaco/types@1.0.48 @0xmonaco/react@1.0.48 @0xmonaco/contracts@1.0.48
```
The MCP server ships at `1.0.48` too; it runs via `npx -y @0xmonaco/mcp-server`, so restarting your assistant picks up the new version. Every SDK bump is a patch release.
One action item, and it is compile-time only: if you switch exhaustively over `ConditionalOrderEventData.timeInForce` or `.reason`, add a `default` branch. Nothing else here is breaking — `availableOrderCollateral` is an optional field, and `useInstruments` is a new export. If you already size orders from `freeCollateral` or `withdrawableCollateral`, read the `availableOrderCollateral` notes above before switching: it answers a different question, it is not additive across buckets, and it is absent on parent rows and fallback reads.
# V1.0.50
Source: https://docs.0xmonaco.com/releases/v1.0.50
# Monaco Protocol SDK v1.0.50
Two changes need a decision before you upgrade. Registering a sweeper deposit address is now authenticated and takes no identity at all — `clientId` and `userAddress` are gone from the request, and the session supplies both — with a new optional `depositTarget` that can point swept deposits at margin collateral instead of the spot wallet. And funding an isolated risk bucket that holds an open position now **commits** the money to that position's stored margin by default, which is the right behavior for an Adjust Margin gesture and the wrong one for a funding loop: those must now pass `applyToPositionMargin: false`. `transferCollateralToMarginAccount` is deprecated in the same move, `@0xmonaco/contracts` gains the deposit-address encoder, and the `account` channel's `marginRatio` documents one more state it can saturate in.
## Breaking
### Sweeper registration is authenticated, and carries no identity
`sdk.sweeper.register` (`POST /api/v1/sweeper/register`; gRPC `SweeperService.Register`; Rust REST `register_deposit_address`) now requires a session, and both `clientId` and `userAddress` have been removed from the request. The application is the one the session was established under and the credited address is the session's own, so the call can only ever register the caller's own address under their own application. The argument is now optional and carries nothing but the target.
```typescript theme={null}
// Before
await sdk.sweeper.register({ clientId, userAddress });
// After
await sdk.login(clientId);
await sdk.sweeper.register();
```
A body that still carries either removed field is rejected with a `400` rather than silently ignored — answering it from the session would hand the caller a deposit address for a different user or application than their own body names. Unauthenticated calls answer `401`, and a session whose application is no longer active answers `403`. The endpoint also declares `500` for a server-side failure and `503` for any point at which the sweeper could not complete the registration — several unrelated conditions share it. Treat a `503` as an **unknown** outcome rather than a negative one: the registration is stored before the reply is composed, so a failure reported after that write is indistinguishable from one reported before it, and the status alone never tells you which happened. Re-register rather than assuming either way — registration is idempotent, and the retry answers `registered` or `already_registered` according to whether the first attempt had landed. Both are success and both carry the same address, so accept either rather than branching on which one you got.
The per-client (IP-keyed) rate limit is replaced by a **service-wide** admission limit on *new* registrations, which answers `429`. A per-caller bound handed every fresh session its own allowance; registrations create durable state, so the bound is now global. Re-registering an existing address is never limited — it creates nothing — and a refused call stores nothing, so `429` is safe to retry. The REST body's `retryAfter` (and gRPC's `RetryInfo` detail) is the limiter's own remaining wait in seconds, not a fixed window: honor it rather than assuming one.
The new optional `depositTarget` (`"spot"`, the default, or `"margin"`; `SPOT`/`MARGIN` on the wire) asks for the parent margin account's collateral, skipping the separate `transferCollateralToParentMarginAccount` step. **The target is part of the address derivation**, so `"spot"` and `"margin"` return two different deposit addresses for the same application and user, each carrying its own request; a `"spot"` address is never credited to margin. Every address derived before this change is unaffected — `"spot"` reproduces the previous bytes exactly. An unrecognized value is rejected (the TypeScript SDK throws before sending; the API answers `400`) rather than falling back to spot, which would return an address pointed at a ledger the caller did not ask for.
`"margin"` is a routing request rather than a guarantee: a deposit the matching engine cannot route to collateral — an unsupported collateral asset, or a margin account that fails validation — is credited to the spot wallet instead, so funds are never stranded. Read the balance back rather than treating the address as proof of where the credit landed. See [Vault](/sdk/typescript/vault#registered-deposit-addresses) and [Registered Deposit Addresses](/developers/vault#registered-deposit-addresses).
## Changed
### Funding an isolated risk bucket commits to the position's margin by default
`transferCollateralToRiskBucket` (`POST /api/v1/margin/risk-buckets/collateral/transfer-in`; gRPC `MarginAccountsService.TransferCollateralToRiskBucket`; the `transfer_collateral_to_risk_bucket` MCP tool) takes a new optional `applyToPositionMargin`, and its default is a behavior change.
* **Omitted or `true`** — the human "Adjust Margin" gesture: the amount is **committed** to the isolated bucket's sole open position's stored margin. Effective leverage drops and stays down, initial margin required rises by the amount, and free collateral is unchanged, so new orders cannot spend it.
* **`false`** — the previous behavior: a plain allocation. Free collateral rises and the next order can spend it.
**Programmatic funding loops that top a bucket up to a free-collateral target must now pass `false`.** Under the default each top-up converts into requirement instead of headroom, and the loop never converges.
A flat isolated bucket has no position to commit to, so both values are a plain allocation there. The cross form has no per-position margin, and the two contracts differ by surface: raw REST and gRPC reject only an explicit `true`, while the TypeScript request type (`never`) and the MCP tool schema reject the field however it is set — **omit it entirely on a cross transfer** rather than passing `false`. Either behavior improves the liquidation cushion identically — the breach check is bucket-equity based.
The response gains an optional `positionMarginCredited` reporting what actually happened: `true` — the credit landed; `false` — a credit was requested (or defaulted) and degraded to a plain allocation because the bucket's persisted position row was stale (a close/reopen race), so re-issue the Adjust Margin once the new row lands if the commitment still matters; absent (never null) on plain allocations, parent deposits, and releases. See [Margin Accounts](/sdk/typescript/margin-accounts) and [Perps Collateral](/developers/perps-collateral).
## Deprecated
### `transferCollateralToMarginAccount`
`transferCollateralToMarginAccount` (`POST /api/v1/margin/accounts/{margin_account_id}/collateral/transfer-in`; gRPC `MarginAccountsService.TransferCollateralToMarginAccount`) is marked deprecated — a JSDoc `@deprecated` in TypeScript, `deprecated: true` in the OpenAPI operation. It overloads two unrelated operations behind one route: with `tradingPairId` it allocates into that pair's isolated risk bucket, without it it performs a wallet-debiting deposit to the parent margin account.
Use `transferCollateralToRiskBucket` for bucket funding — which, as of this release, applies the same position-margin credit — and `transferCollateralToParentMarginAccount` for wallet-to-margin deposits. The endpoint keeps working and no removal date is scheduled.
## Added
### The deposit-address encoder is exported from `@0xmonaco/contracts`
`predictSweeperAddress` accepts an optional `depositTarget` (`"spot"`, the default, or `"margin"`), and `encodeDepositApplicationData(clientId, target)` plus the `DepositTarget` type are now exported from `@0xmonaco/contracts`. Omitting the target reproduces the previous derivation byte-for-byte, so existing calls are unaffected; pass the same target the address was registered with, or the prediction points at the other ledger's address.
`encodeDepositApplicationData` is the canonical encoder for a deposit's on-chain `applicationData` — the bytes a sweeper address is a hash of, and the counterpart of the backend's own encoder. `@0xmonaco/core` consumes it internally rather than keeping a second copy, and `@0xmonaco/types` re-exports `DepositTarget` from the same place: the value selects both a direct deposit's on-chain routing hint and a sweeper address's derivation, and two definitions could disagree about the bytes an address is derived from. The importable path is `@0xmonaco/contracts` — core's package root does not re-export it. `DepositTarget` itself is untouched: it changed packages, not shape, so its name and its two members are the same and it is still reachable from `@0xmonaco/types`.
Both `predictSweeperAddress` and `encodeDepositApplicationData` throw on a target that is not exactly `"spot"` or `"margin"` rather than treating anything unrecognized as spot. The likely mistake is the wire form `"MARGIN"` copied from the API reference; without the check it would derive a real, fundable **spot** address while the caller believed it was a margin one, and no server round-trip would reveal it.
## Fixed
### `marginRatio` on the `account` channel saturates on dust equity too
The documented contract of `AccountEvent`'s `marginRatio` widens. The maximum-distress sentinel (`79228162514264337593543950335`) appears whenever a live maintenance requirement makes the ratio impossible to compute or represent — not only for an account still owing one at zero or negative equity, but equally at near-zero **positive** equity, where the true `maintenance / equity` exceeds the decimal range. Previously the producer crashed on that state instead of emitting a frame. An account carrying no maintenance requirement is unchanged and still reports `0`, whatever its equity: an empty account is not distressed.
No type change, and consumers that already clamp the sentinel for display need no code change: treat any value at or above the liquidation threshold as maximal distress rather than distinguishing the sentinel's causes. See [Account Channel](/sdk/typescript/websockets#account-channel).
## Upgrade
```bash theme={null}
npm install @0xmonaco/core@1.0.50 @0xmonaco/types@1.0.50 @0xmonaco/react@1.0.50 @0xmonaco/contracts@1.0.50
# or bun
bun add @0xmonaco/core@1.0.50 @0xmonaco/types@1.0.50 @0xmonaco/react@1.0.50 @0xmonaco/contracts@1.0.50
```
The MCP server ships at `1.0.50` too; it runs via `npx -y @0xmonaco/mcp-server`, so restarting your assistant picks up the new version. Every SDK bump is a patch release. There is no `1.0.49` — that version number was never released.
Two action items. If you call `sweeper.register`, log in first and drop `clientId` and `userAddress` from the body; a partially-migrated body is a `400`, not a silent ignore. And if any code path funds an isolated risk bucket to reach a free-collateral level, pass `applyToPositionMargin: false` — the new default commits the amount to the open position's margin instead, and a loop that measures free collateral will never converge. Deriving deposit addresses off-chain? Pass the same `depositTarget` you register with, and import `encodeDepositApplicationData` from `@0xmonaco/contracts`.
# V1.0.51
Source: https://docs.0xmonaco.com/releases/v1.0.51
# Monaco Protocol SDK v1.0.51
This release covers the consumer-facing changes since v1.0.50. Three migrations come first: stop scaling WebSocket fill `fee` values yourself, update order-event field and null checks, and give switches over WebSocket value fields a `default` branch. Orders gain a shared reconciliation `version`, connection-scoped WebSocket errors reach an `onError` callback, and React order state handles unfamiliar values and REST repair more reliably. Timestamp formatting, portfolio valuation, subscription snapshots, liquidation cleanup, and fee refresh also receive corrections; `batchCancelAll` now refuses requests above 20,000 active orders without cancelling any.
## Breaking
### Fill `fee` is a decimal quote-asset amount
On `OrderFilled`, `OrderPartiallyFilled`, and `OrderMatched`, `fee` now reports the normalized decimal quote-asset amount: `"0.004178"` USDC instead of the old raw `"4178"` at six quote decimals. **Stop dividing `fee` by `10^quoteDecimals`.** The new `feeRaw` (wire `fee_raw`) carries the integer raw quote-unit value if your accounting works in raw units; negative values mean a maker rebate.
Two existing properties still matter. This is the **Monaco fee component only**, excluding the application taker fee, so it need not equal the order's `totalTakerFees`. A taker event aggregates the fees of every trade it covers; a maker event covers one trade. `feeRaw` is required on `OrderMatched` and optional in the published `OrderFilled` and `OrderPartiallyFilled` types. See [Order Events](/sdk/typescript/websockets#order-event-types).
### Order-event types now distinguish absent fields from null values
`takerOrderId` and `makerOrderId` are removed from the three fill-event types: neither existed on the wire. Use `orderId` for the event's own order and `tradeId` for its trade reference. `totalFilled` is also removed from those fill types; use `filledQuantity` for the cumulative order fill and `lastFillQuantity` for this event's increment. `totalFilled` remains on cancellation and rejection payloads, where the server sends it.
`OrderMatched` gains its missing execution fields and now describes the taker payload exactly. Update fixtures and consumers to distinguish these groups:
* **Required:** `role`, `status`, `tradesCount`, `filledQuantity`, `filledQuantityRaw`, `remainingQuantity`, `remainingQuantityRaw`, `fee`, and `feeRaw`.
* **Always present, nullable:** `tradeId`, `executionPrice`, `executedAt`, `averageFillPrice`, `referencePrice`, `bestPrice`, `worstPrice`, `actualSlippageBps`, and `maxSlippageBps`. The trade fields are null when no trade is covered; price and slippage fields are null when unavailable.
* **Optional incremental-fill fields:** `lastFillQuantity` and `lastFillQuantityRaw`, omitted when the event carries no incremental fill.
`OrderExpired` likewise makes `filledQuantity`, `filledQuantityRaw`, `remainingQuantity`, `remainingQuantityRaw`, and `averageFillPrice` required `string | null`: the keys are always present. Branch on null rather than assuming an absent key. `OrderFilled` and `OrderPartiallyFilled` still cover both maker and taker payloads, so their execution fields are not tightened to the taker-only shape. See [WebSockets](/sdk/typescript/websockets#order-event-types) for the individual payloads.
### WebSocket value fields accept unfamiliar server strings
Many event value fields widen from a closed alias to `KnownAlias | (string & {})`, matching the parser's delivery of unknown values. This affects order values (including status, role, terminal reason, side, type, time in force, and trading mode), conditional-order values, position side/status, market status, TWAP values, instrument change kinds, OHLCV interval/mode/price type, orderbook mode/side, trade mode/maker side, and balance reasons.
The finite aliases remain exported for autocomplete, narrowing, REST filters, and validation. Add a `default` branch or an explicit narrowing check before assigning a received value to a closed model type. The broad `OrderEvent.eventType` also widens, but **named variants keep their closed `eventType` discriminants** — narrowing `OrderPlacedEvent` and its siblings still works.
## Added
### Connection-scoped WebSocket errors reach `onError`
The `ws` configuration accepts `onError({ code, message })`, also available on `createMonacoWebSocket` options. `@0xmonaco/types` exports `WebSocketErrorCode`, `WebSocketErrorInfo`, and `ErrorHandler`. Server `Error` frames belong to the connection and match no channel subscription; previously they reached no consumer handler.
Known codes are `AUTH_REQUIRED`, `AUTH_FAILED`, `CONNECTION_LIMIT`, `SUBSCRIPTION_LIMIT`, `INVALID_SUBSCRIPTION`, `INVALID_MESSAGE`, `SESSION_INVALID`, and `SNAPSHOT_UNAVAILABLE`. Recovery depends on the code:
* `AUTH_REQUIRED` means the server rejected the subscription. Authenticate, release the last local handler for that channel, then subscribe again; authentication alone leaves the locally registered subscription inactive on the server.
* `SESSION_INVALID` requires re-authentication.
* `SNAPSHOT_UNAVAILABLE` leaves the subscription active and live diffs flowing, but without its baseline. Fetch a REST baseline and reconcile it with live state.
Unknown codes are delivered unchanged, so keep a default case. See [Server Error Frames](/sdk/typescript/websockets#server-error-frames).
### A shared `version` for order reconciliation
Order detail and list reads (`GetOrder` / `ListOrders`, including `sdk.trading.getOrder()` / `getPaginatedOrders()`), every order WebSocket event, and the `orders` subscription snapshot now carry an optional `version`. It is an opaque comparator for states of **the same order**: a higher value is newer, gaps are normal, and values from different orders are not comparable.
On reconnect, keep applying live order events and accept a REST snapshot row when `snapshot.version >= local.version`. **Use `>=`, not `>`**: one sequencer step can emit placement and fill events with the same version, while the REST row represents that step's final state and wins the tie. An absent version means unknown, never zero; fall back to buffering and replaying events across the fetch for those rows.
The resting snapshot cannot recover orders that stopped resting during the disconnect. Page the execution-time-ordered `sdk.profile.getUserTrades` back through the gap for fills, and use targeted `sdk.trading.getOrder` reads for locally resting orders missing from the snapshot to resolve cancellations and expiries. Ordering the order list by creation time cannot substitute for the trade tape. See the [Market-Maker Runbook](/developers/market-maker-runbook#4-reconnect-and-resync).
### Conditional-order versions on REST and subscription snapshots
Conditional-order REST reads and the `conditional_orders` subscription snapshot gain the same kind of optional, opaque `version`. Compare versioned snapshots for the same conditional order with `snapshot.version >= local.version`; absent means unknown, gaps are normal, and comparison across conditional orders or against ordinary orders is invalid.
**Live conditional-order events deliberately omit `version`.** Apply them as state updates without version ranking. The subscription snapshot reads from the database, so a transition during that read can fall into a database-lag gap; re-read affected rows when their state matters at reconnect. See [Conditional Orders Channel](/sdk/typescript/websockets#conditional-orders-channel).
### Post-only state and missing raw execution fields
Order events expose `postOnly?: boolean` on placement, taker payloads, partial cancellation, cancellation, and expiry. A carrying variant sends `true` or omits the key; maker fills and rejections never carry it. An admitted post-only order rests and fills as a maker, so **retain the order-level flag from placement or REST** — absence on a maker fill does not mean false. `useUserOrders` now carries this state onto its order rows and preserves it through updates.
`OrderRejected` declares `totalFilledRaw` and `remainingQuantityRaw`; `OrderExpired` declares `filledQuantityRaw` and `remainingQuantityRaw`. These previously invisible fields carry integer base-unit precision alongside their decimal counterparts. The missing `OrderMatched` execution fields are covered by the exact-shape migration above.
## Changed
### Selected persisted order timestamps use fixed UTC microseconds
REST read models now render ordinary-order `createdAt`, `updatedAt`, and `expirationDate`; every conditional-order timestamp; persisted TWAP window and lifecycle timestamps; and application order-view `createdAt` / `updatedAt` with exactly six fractional digits and a `Z` suffix, for example `2026-09-05T01:23:45.123456Z`. This replaces variable fractional widths and `+00:00` on those fields. RFC 3339 parsers keep working; exact-string fixtures may need updates.
Ordinary-order lifecycle stamps — `cancelledAt`, `filledAt`, `expiredAt`, `submittedAt`, and `acknowledgedAt` — retain chrono's variable fractional width, although they use a `Z` suffix. The guarantee also does not extend to `CreateTwapOrderResponse.firstSliceAt` or `.endTime`, which retain their separate rendering.
Formatting does not align the clocks behind order detail and list `updatedAt` values. Use `version` for reconciliation rather than comparing those timestamps.
### `batchCancelAll` refuses more than 20,000 active orders
`sdk.trading.batchCancelAll` now caps a request at **20,000 active orders**. Above the cap it cancels nothing and returns REST `400` / gRPC `InvalidArgument`. Treat this as a rejected operation, not a partial cancellation. See [Trading](/developers/trade#batch-cancel) for the request's filtering options.
## Fixed
### WebSocket parsers retain fields and unfamiliar values
The movements parser now delivers `symbol`, `balanceBeforeRaw`, `balanceAfterRaw`, `lockedBeforeRaw`, and `lockedAfterRaw`, which were declared but discarded while building callbacks. TWAP updates with an unfamiliar `state` and instrument updates with an unfamiliar `change` no longer lose the whole frame. OHLCV preserves an unfamiliar `priceType` instead of blanking the field, so wildcard subscriptions can distinguish newly introduced series.
### `useUserOrders` narrows values and merges REST repair with live state
The hook no longer inserts an unknown status into the closed `Order` model: it uses the status implied by the event type or retains the previous status when no status is implied. Placements remain `SUBMITTED`. A placement with an unknown `orderType`, `side`, or `tradingMode` cannot honestly form an order row, so the hook declines it and schedules a coalesced REST repair; an unknown optional `timeInForce` remains absent.
Refresh and repair responses merge with live state instead of replacing updates that arrived while REST was in flight. The released hook still reconciles using `updatedAt`; the new order `version` contract above is available to consumers implementing their own reconciliation. See [`useUserOrders`](/sdk/react/hooks/use-user-orders).
### Portfolio performance can return partial withdrawal valuation
An external withdrawal without a usable historical price no longer fails the entire portfolio-performance response solely for that missing valuation. The unpriceable movement is omitted from withdrawal totals and the deposit totals that depend on them, and a successful response reports `valuationStatus: "partial"`. Recognized quote assets retain unit pricing; reconciliation errors can still fail the request. See [Portfolio](/sdk/typescript/portfolio).
### Subscription snapshots, liquidation cleanup, and fee refresh
Order, conditional-order, and position subscription snapshots now use stable keyset paging, preventing concurrent updates from shifting page boundaries and duplicating or skipping rows. This improves paging consistency; it does not eliminate the conditional-order database-lag gap described above.
When a liquidation maker fill closes a position, its remaining reduce-only orders are now cancelled with `POSITION_CLOSED`, releasing their reserves while preserving order history and PnL enrichment.
Fee-schedule refresh uses bounded timeouts and a consistent repeatable-read snapshot, retains the last known good schedule on failure, and retries. Transient refresh trouble no longer freezes the refresh loop or mixes fee tiers from different reads.
## Upgrade
```bash theme={null}
npm install @0xmonaco/core@1.0.51 @0xmonaco/types@1.0.51 @0xmonaco/react@1.0.51 @0xmonaco/contracts@1.0.51
# or bun
bun add @0xmonaco/core@1.0.51 @0xmonaco/types@1.0.51 @0xmonaco/react@1.0.51 @0xmonaco/contracts@1.0.51
```
Before upgrading, remove manual scaling of fill `fee`, migrate the removed and nullable order-event fields, and narrow unfamiliar WebSocket values before assigning them to closed models. Register `onError` so rejected subscriptions and missing baselines become visible, and use same-order `version` comparisons plus gap recovery when implementing reconnect reconciliation.
# V1.0.53
Source: https://docs.0xmonaco.com/releases/v1.0.53
# Monaco Protocol SDK v1.0.53
This release covers the consumer-facing changes since v1.0.51. Three migrations come first: switch sweeper derivation to the factory's `forwarder` without exposing a new address for an existing registration until its stored row is migrated, remove `selectedTradingPairIds` from cross risk-bucket funding, and treat simulated `estimatedFee` as a signed whole-order estimate that may be absent. Order creation gains durable idempotency keys, stateful WebSocket subscriptions gain typed baseline callbacks, and portfolio margin rows identify their own scope. Order admission, triggered-close event delivery, first-use isolated simulation, and isolated-position backing are also corrected.
## Breaking
### Sweeper addresses now derive through the factory forwarder
`predictSweeperAddress` and `sweeperCloneInitCode` now take the factory's `forwarder` address instead of its Sweeper `implementation`. `SweeperAddressInput.implementation` is renamed to `forwarder`, and every derived address changes.
Read the input from `factory.forwarder()`. Fresh registrations persist and monitor the new address. An existing registration created before v1.0.53 needs an operator-side stored-row migration before you display or fund its new address: calling register returns and monitors the forwarder-derived address in the running resolver, but does not update that row's stored `sweeperAddress`, so a resolver restart reloads only the old address. Re-registration alone is not restart-durable. The forwarder is fixed while it resolves the current implementation per call, so later logic upgrades do not move deposit addresses. `sweeperImmutableArgs(user, clientId, hubToken?)` also gains the optional hub-token input; user-facing deposit addresses use the zero address.
The regenerated `SWEEPER_ABI` and `SWEEPER_FACTORY_ABI` include the new multi-chain entrypoints and configuration surface. See [Registered Deposit Addresses](/sdk/typescript/vault#registered-deposit-addresses).
### Cross risk-bucket funding no longer declares market scope
For `marginMode: "CROSS"`, `sdk.marginAccounts.transferCollateralToRiskBucket` now accepts only `{ asset, amount, marginMode: "CROSS" }`. Remove `selectedTradingPairIds`: the TypeScript SDK rejects it before sending, and `transfer_collateral_to_risk_bucket` removes it from the MCP tool.
Cross scope is derived from trading. A pair joins when a cross order on it succeeds. Removal after its positions close is lazy: the pair remains listed until a later successful cross order on a different pair reconciles the bucket, so a funding call has no scope to replace. Read current membership from the cross row returned by `listMarginAccounts()` or from the transfer response. The raw REST field remains deprecated for compatibility and is ignored in cross mode; the generated gRPC field is deprecated. `simulateRiskBucketOrderRisk` is unchanged and still requires the pair list in cross mode.
This also lets parent-margin simulation and pair-filtered margin summaries resolve a never-traded pair to the active cross risk bucket. See [Margin Accounts](/sdk/typescript/margin-accounts#transfer-collateral-to-a-risk-bucket).
### `estimatedFee` is a signed whole-order estimate
`estimatedFee` on all three authenticated order-risk simulation responses now estimates fees for the order's complete size: taker pricing, including the application taker fee, for the quantity that would cross immediately; maker pricing for the quantity that would rest. A fully crossing order is unchanged.
The field stays an optional string, so this change does not produce a compile error:
* A negative value is a net maker rebate. Do not parse or render it as an unsigned cost.
* Absence means the engine could not price the order. Treat it as unknown, never zero.
* The estimate remains available for reduce-only orders and can be present when `accepted` is false. It assumes a resting remainder eventually fills.
The affected REST endpoints are `POST /api/v1/margin/accounts/{margin_account_id}/simulate-order-risk`, `POST /api/v1/margin/parent-margin-account/simulate-order-risk`, and `POST /api/v1/margin/risk-buckets/simulate-order-risk`; the matching gRPC methods are on `MarginAccountsService`. See [Order Risk Simulation](/sdk/typescript/margin-accounts#simulate-order-risk).
## Added
### Durable order-submission idempotency
Single order create and each batch-create item accept an optional `idempotencyKey`. For 24 hours after acceptance, retrying the same normalized payload with the same key returns the original committed submission result without executing again, including after the order fills, is cancelled, or the matching engine restarts. Reusing the key with different details returns REST `409 IDEMPOTENCY_KEY_CONFLICT` / gRPC `ALREADY_EXISTS`.
The TypeScript SDK generates a UUID when the key is omitted, including one per batch item. That protects one SDK invocation; a later call receives a new key. Generate, persist, and reuse an explicit key when recovery must survive another call or process restart. `clientOrderId` remains a separate reusable correlation label and cannot prevent duplicate execution.
Raw REST, generated Rust/Python, and gRPC callers must generate their own keys. The field remains optional during this rollout; requests rejected before acceptance do not reserve it. See [Safe Retries with Idempotency Keys](/developers/trade#safe-retries-with-idempotency-keys).
### Typed WebSocket baseline callbacks
Stateful `sdk.ws` subscriptions accept an optional trailing `onSnapshot` callback. It receives the subscribe-time baseline as one typed array, separate from the live event handler. Use the array to replace baseline state; reducing it as live deltas can double-apply data. Empty state arrives as `onSnapshot([])`, malformed arrays are withheld rather than delivered partially, and reconnect resubscription re-arms the callback.
The server currently sends baselines for orders, user orders, balances, conditional orders, positions, and instruments. The other stateful method signatures are ready for future server arms; their callbacks remain inert until those arms ship. Event streams (`orderbook`, `ohlcv`, `trades`, and `movements`) have no `onSnapshot`.
`@0xmonaco/types` adds `SnapshotHandler` and `OrderSnapshotItem`. `useInstruments`, `useUserBalances`, and `useUserOrders` now seed and repair state from these baselines, including reconnect omissions and version-ranked order reconciliation. See [Subscribe-Time Snapshots](/sdk/typescript/websockets#subscribe-time-snapshots).
### Portfolio risk-bucket rows identify their scope
Each `PortfolioRiskBucket` returned by `sdk.portfolio.getMargin()` now includes required `marginMode` (`"ISOLATED" | "CROSS"`) and optional `tradingPairId`, present only for isolated rows. Callers can identify each portfolio row's mode and isolated-pair scope without another lookup. Cross multi-market membership is not carried by this response; read `selectedTradingPairIds` from the cross risk-bucket row returned by `sdk.marginAccounts.listMarginAccounts()`.
The response's `positions.cross` and `positions.isolated` counts span all of the user's margin accounts, while `riskBuckets` belongs to the oldest parent account. Do not treat grouping the rows by `marginMode` as a checksum over those counts. See [Portfolio Margin](/sdk/typescript/portfolio#get-margin).
## Changed
### Matching-engine calls have bounded server deadlines
The matching-engine's internal gRPC listener now defaults to 1,024 concurrent handlers and HTTP/2 streams per connection, a 30-second deadline for ordinary RPCs, and a 600-second deadline for batch-order RPCs. State-snapshot capture stays single-flight and has no deadline by default so large proposer bootstraps can finish.
For public API callers, an internal deadline expiry is classified as REST `503` / gRPC `Unavailable`. A mutating request has an **unknown outcome** at that point: submitted sequencer or blocking work may still commit after the response times out. Reconcile state before retrying. For order creation, reuse the same `idempotencyKey`; never substitute a new key for an unresolved submission. Keyed `batchCreate` is the exception to the transport status: it returns overall REST `200` / gRPC `OK` and reports an expired item as `results[].error.code === "SERVICE_UNAVAILABLE"`. The TypeScript SDK keys every item, so inspect every result and retain the original key for any item you retry.
## Fixed
### Small collateral gaps no longer discard otherwise admissible orders
A bucket-scoped resting maker fill can now cover a collateral shortfall from the parent's unallocated capacity, capped at the position's `initialMarginRequired - maintenanceMarginRequired`. The resting remainder of a partially crossing taker can fund only deterioration caused by that order since admission; it stops at the same liquidation bound and is also capped by the remaining admission budget — parent capacity for an existing bucket, or the unused fee-reserve allowance for a bucket the order created. Previously a small adverse mark move could silently remove the maker or refuse the taker remainder even when repair was admissible. The tightest cap and liquidation freezes still win; there is no client switch to enable.
### Triggered TP/SL closes publish the full execution event tail
A take-profit or stop-loss close now publishes the same orderbook, order, trade, OHLCV, position, and balance updates as an ordinary execution. Previously the close could execute while subscribers received the conditional-order trigger but missed some downstream state updates. Continue subscribing to both conditional-order events and the ordinary state channels: the former confirms the trigger, while the latter carries execution and resulting state. See [Conditional Order Lifecycle](/developers/trade#conditional-order-lifecycle).
### First-use isolated order-risk simulation works before a bucket exists
`simulateRiskBucketOrderRisk({ marginMode: "ISOLATED", ... })` no longer fails when the trading pair has no isolated risk bucket yet. The engine models the bucket virtually with the collateral a real first order would attach, so `equityAfter` and `freeCollateralAfter` describe the bucket that placement would create. An account unknown to the risk engine because it has never been funded still reports `margin account not found`.
### Open isolated positions report their current backing
`isolatedMargin` on open isolated positions now reports the position's current backing: allocated risk-bucket collateral adjusted by settled realized PnL and funding, net of collateral reserved for open orders, and clamped at zero. The previous value was stale posted margin after partial closes or partial liquidations. REST reads, gRPC reads, the positions snapshot, and live `position_update` frames agree while live risk state is available; a persisted fallback is gross of open-order reserves. Cross positions and terminal rows retain their existing meanings. See [Positions](/sdk/typescript/positions#read).
## Upgrade
```bash theme={null}
npm install @0xmonaco/core@1.0.53 @0xmonaco/types@1.0.53 @0xmonaco/react@1.0.53 @0xmonaco/contracts@1.0.53
# or bun
bun add @0xmonaco/core@1.0.53 @0xmonaco/types@1.0.53 @0xmonaco/react@1.0.53 @0xmonaco/contracts@1.0.53
```
Before upgrading, pause deposits for every pre-v1.0.53 sweeper registration until Monaco confirms its stored row was migrated; then derive its address from `factory.forwarder()`. Remove `selectedTradingPairIds` from cross funding, and update fee displays for negative and absent `estimatedFee` values. Persist explicit idempotency keys when retries cross calls or restarts, and consume `onSnapshot` as a replaceable baseline rather than as live events.
# V1.0.54
Source: https://docs.0xmonaco.com/releases/v1.0.54
# 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](/sdk/typescript/error-handling#retrying-order-mutations-safely) and [Safe Retries with Idempotency Keys](/developers/trade#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](/developers/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](/websocket#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](/sdk/typescript/websockets#order-event-types).
## 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](/developers/trade#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](/sdk/typescript/websockets#slow-client-disconnect-close-code-1013).
### 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](/sdk/typescript/portfolio). 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
```bash theme={null}
npm install @0xmonaco/core@1.0.54 @0xmonaco/types@1.0.54 @0xmonaco/react@1.0.54 @0xmonaco/contracts@1.0.54
# or bun
bun add @0xmonaco/core@1.0.54 @0xmonaco/types@1.0.54 @0xmonaco/react@1.0.54 @0xmonaco/contracts@1.0.54
```
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.
# V1.0.55
Source: https://docs.0xmonaco.com/releases/v1.0.55
# Monaco Protocol SDK v1.0.55
This release covers the consumer-facing changes since v1.0.54. Nothing in it changes a request or response field, and nothing changes for a client that upgrades and does nothing else. REST session authentication now accepts a second, opt-in signing protocol — RFC 9421 HTTP message signatures over the same Ed25519 session keys — alongside the legacy `X-Monaco-*` headers, which remain supported with no retirement date. The TypeScript and Rust SDKs can produce the new signatures, but both default to legacy, so opting in is a deliberate per-application step and one you should only take against a gateway that already verifies it. Alongside that, the OpenAPI specification now declares an authentication requirement on 24 endpoints that previously declared none — a specification correction, not a change in what the gateway enforces.
## Added
### Opt-in RFC 9421 request signing
REST session authentication accepts RFC 9421 HTTP message signatures as an alternative to the legacy `X-Monaco-PublicKey` / `X-Monaco-Timestamp` / `X-Monaco-Signature` headers. Both protocols use the same wallet-authorized Ed25519 session keys and run the same session expiry, revocation, delegated-agent permission, and account-restriction checks. WebSocket and gRPC signing are unchanged.
A signed request carries three headers:
```http theme={null}
Content-Digest: sha-256=:BASE64_SHA256_OF_BODY:
Signature-Input: monaco=("@method" "@path" "@query" "content-digest");created=1788868800;keyid="SESSION_PUBLIC_KEY_HEX";alg="ed25519"
Signature: monaco=:BASE64_ED25519_SIGNATURE:
```
Monaco verifies a bounded application profile rather than arbitrary RFC 9421 input. The signature label is `monaco` and exactly one dictionary member is accepted; the four covered components appear in the order shown; `created`, `keyid`, and `alg` are required; and extra components, extra labels, extra parameters, or another digest algorithm are rejected. `created` is an integer Unix timestamp in **seconds** within 30 seconds of server time — legacy timestamps are milliseconds. `keyid` is the registered session public key as 64 lowercase hexadecimal characters. `@query` begins with `?`, keeps raw ordering, duplicates and percent encoding, and is `?` when the query is absent. `Content-Digest` is RFC 9530 SHA-256 over the exact body bytes, including the empty body on a `GET`.
Presence of `Signature` or `Signature-Input` selects RFC verification, and failure never falls back to legacy: there is no automatic protocol downgrade. Sending `Signature` or `Signature-Input` alongside any `X-Monaco-*` header is rejected as `401 Unauthorized`. `Content-Digest` on its own is ordinary HTTP integrity metadata — it neither selects RFC verification nor overrides the body hash legacy verification computes independently.
The 30-second window bounds how long a captured signature stays usable; it does not stop an identical signed request from being accepted again inside that window, and Monaco keeps no nonce or replay cache. Use the endpoint's idempotency mechanism for exactly-once behavior, and when re-signing an aged-out retry, keep the original `idempotencyKey` and payload. See [HTTP message signatures](/reference/http-message-signatures) for the full profile, signature base, and migration sequence.
### `SDKConfig.requestSigning` selects the protocol in TypeScript
`createMonacoSDK` (and `new MonacoSDK`) accepts `requestSigning: "legacy" | "rfc9421"`, defaulting to `"legacy"`. The `RequestSigning` type is exported from `@0xmonaco/types`.
```typescript theme={null}
const sdk = createMonacoSDK({
network: "staging",
seiRpcUrl: "https://your-sei-rpc.example",
requestSigning: "rfc9421",
});
```
In `rfc9421` mode the SDK signs the final URL path and query plus the serialized body, requires the body to be a string, sets `redirect: "error"` on authenticated requests so a signed request is never forwarded unchanged, and throws if the caller supplies any signature header itself. Public and backend-key requests keep their existing authentication either way. See [TypeScript migration](/reference/http-message-signatures#typescript-migration).
### `Client::new_authenticated` in the Rust REST SDK
The Rust REST SDK gains a signing client constructor:
```rust theme={null}
use monaco_sdk::{Client, RequestSigning, SessionSigner};
let signer = SessionSigner::new(session_private_seed, RequestSigning::Rfc9421);
let client = Client::new_authenticated("https://staging.apimonaco.xyz", signer);
```
It signs the session-authenticated operations the OpenAPI contract classifies and leaves public and backend-key operations unsigned. Authenticated clients disable HTTP redirects, which is why the constructor is available on native targets only — redirect policy is not enforceable on `wasm32`. Existing unsigned constructors are unchanged. `RequestSigning::Legacy` selects the legacy headers for a gateway that has not migrated, and the lower-level `SessionSigner::sign_at` helper signs a reusable `reqwest` request, with the caller responsible for preserving the signed bytes and disabling redirects. Streaming bodies are rejected rather than signed as empty. See [Rust migration](/reference/http-message-signatures#rust-migration) and [Sign Authenticated Requests](/sdk/rust-rest#sign-authenticated-requests).
## Fixed
### The specification declares authentication on 24 previously undeclared endpoints
The OpenAPI specification gains a `monacoHttpSignature` security scheme next to the existing `monacoSignature`, and every session-authenticated operation now lists both. While making that change, 24 operations were found to declare **no** security requirement at all, so the generated API reference and any client generated from the specification presented them as public:
* seven endpoints under `/api/v1/positions` — `GET /api/v1/positions`, `GET /api/v1/positions/history`, `GET /api/v1/positions/{positionId}`, `GET /api/v1/positions/{positionId}/risk`, `POST /api/v1/positions/batch-close-all`, `POST /api/v1/positions/{positionId}/close`, and `POST /api/v1/positions/{positionId}/tp-sl`. `GET /api/v1/positions/{positionId}/pnl/history` already declared its requirement and is unaffected
* all fourteen endpoints under `/api/v1/margin` — the margin account and parent margin account reads and their movements, `GET /api/v1/margin/collateral/available`, the collateral transfer-in and transfer-out endpoints, and the three `simulate-order-risk` endpoints
* the three conditional order endpoints, `GET /api/v1/orders/conditional`, `GET /api/v1/orders/conditional/{conditionalOrderId}`, and `DELETE /api/v1/orders/conditional/{conditionalOrderId}`
**The gateway always required a session signature on all of them** — this corrects the published contract, not the enforced one, so a client that was already signing these calls needs no change. A generated client that relied on the specification to decide what to sign was not signing them, and now will. A test asserts the specification's declared authentication against the gateway's registered route table, so an endpoint can no longer ship with the wrong contract.
### Request-body buffering is scoped to session-authenticated routes
The gateway buffered and hashed every request body globally so that signature verification could cover it. That buffer is now mounted only on session-authenticated resources. Signed routes still read and hash their bodies under the same 2 MiB bound; public and backend-key routes use their normal extractor limits instead. No request or response shape changes, and body hashing is not removed from any signed route.
## Upgrade
```bash theme={null}
npm install @0xmonaco/core@1.0.55 @0xmonaco/types@1.0.55 @0xmonaco/react@1.0.55 @0xmonaco/contracts@1.0.55
# or bun
bun add @0xmonaco/core@1.0.55 @0xmonaco/types@1.0.55 @0xmonaco/react@1.0.55 @0xmonaco/contracts@1.0.55
```
Upgrading changes nothing on its own: `requestSigning` defaults to `"legacy"` and the Rust SDK's existing constructors are untouched. Before selecting `rfc9421`, confirm the gateway you target verifies it, because there is no automatic downgrade — a client that opts in early fails closed. Never mix the two credential sets on one request. If you sign requests with your own client rather than an SDK, read the profile end to end before implementing it: the covered-component list, their order, the seconds-based `created`, and the exact digest encoding are all verified strictly. If you generate a client from the OpenAPI specification, regenerate it so the 24 corrected endpoints are signed, and audit any client generated before v1.0.55 to confirm it was not calling them unsigned.
# V1.0.56
Source: https://docs.0xmonaco.com/releases/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.
# V1.0.57
Source: https://docs.0xmonaco.com/releases/v1.0.57
# Monaco Protocol SDK v1.0.57
This release covers the consumer-facing changes since v1.0.56. The order listing is now unified: `GET /api/v1/orders` returns book orders, TWAP parents and conditional orders as one timestamp-sorted stream, and `orderType` gains two response-only values to name each row's kind — code that switches exhaustively on it must handle them. `@0xmonaco/contracts` ships a breaking sweeper ABI change that binds a quoted sweep to an exact amount. Batch replace now scopes every pre-sequencing validation failure to its own item instead of rejecting the whole batch, so a malformed item comes back as a failed item inside a `2xx` rather than a top-level rejection — read `results[]` per item. A TWAP parent's resting slice child can no longer be cancelled directly, closing a trap that charged taker fees for an order the caller believed cancelled. Unused execution collateral now returns to the margin account after any positive fill on a first-use isolated market order, not only a full one. And authenticated reads now draw a per-account budget, measured but not yet enforced.
## Breaking
### `SweepParams` binds a sweep to an exact amount
`SWEEPER_ABI` and `SWEEPER_FACTORY_ABI` change shape. `SweepParams` gains a leading `amount` field, so the complete tuple is now `(uint256 amount, uint256 minSwapOut, uint256 minBridgeOut, bytes lzExtraOptions)` — four fields, where it previously held the latter three. Calldata encoded against the previous shape produces arguments the new contracts reject — re-encode before upgrading.
`amount` is the exact balance the quote was computed for. The sweep moves exactly that much and leaves any excess — a deposit that landed after the quote — for the next, re-quoted sweep, so the `minSwapOut` / `minBridgeOut` minimums always cover the full swept amount and a later arrival can never dilute them. If the balance has fallen below the quoted amount by the time the sweep executes, the call reverts with the new `StaleQuote` error rather than sweeping less against minimums quoted for more; re-quote and retry.
`amount: 0n` binds nothing and sweeps the execution-time balance. It is accepted **only** on the direct CCTP route, whose burn is exact-amount and carries no minimum that could go stale. Every other route — Stargate, swap-then-bridge, and the hub unswap — rejects the zero sentinel with the new `QuoteNotBound` error. A sweep against an empty balance still reverts with `NothingToSweep`.
Both new errors are declared on `SWEEPER_ABI` only. `SWEEPER_FACTORY_ABI` gains the `amount` field on its `sweepWithParams` input but declares neither error, so a decoder built from the factory ABI alone cannot name a `StaleQuote` or `QuoteNotBound` revert bubbled up from the sweeper — give it both ABIs. `predictSweeperAddress(...)` and `encodeDepositApplicationData(...)` are unaffected — address derivation does not read `SweepParams`. See [Registered Deposit Addresses](/sdk/typescript/vault#registered-deposit-addresses).
## Changed
### One order listing for every kind of order
`GET /api/v1/orders` (gRPC `ListOrders`; `sdk.trading.getPaginatedOrders` and `iterateOrders`) returns book orders, TWAP parents and conditional orders in one stream sorted by timestamp. There is no separate open-orders endpoint: filter `status` to `SUBMITTED,PARTIALLY_FILLED` for the working set across all three kinds, in one request rather than one per kind.
```typescript theme={null}
const { orders } = await sdk.trading.getPaginatedOrders({
status: ["SUBMITTED", "PARTIALLY_FILLED"],
});
for (const order of orders) {
switch (order.orderType) {
case "TWAP":
console.log("twap", order.twap?.slicesPlaced, "of", order.twap?.plannedSlices);
break;
case "CONDITIONAL":
console.log("conditional", order.conditional?.triggerPrice);
break;
default:
console.log("book order", order.price, order.quantity);
}
}
```
`orderType` names each row's kind and gains two response-only values, `TWAP` and `CONDITIONAL`. Neither can be placed: order creation still takes `LIMIT` or `MARKET`, and the two new kinds are created through their own endpoints. In `@0xmonaco/types` this is a type-level change — `Order.orderType` widens from `OrderType` to the new `ListedOrderType` (`"LIMIT" | "MARKET" | "TWAP" | "CONDITIONAL"`), while `OrderType` itself is unchanged and remains what you can place. An exhaustive `switch` or a `never` exhaustiveness check on `order.orderType` stops compiling until it handles the two new values.
A row of either kind carries its full detail under the new optional `twap` / `conditional` fields — the same shapes `GET /api/v1/orders/twap/{twapOrderId}` and the conditional-order endpoints return, so a client already handling those needs no new parsing. Exactly one is present, and only when `orderType` names that kind; a book order carries neither. The shared fields are populated from that detail, so a list renders without special-casing: a parent's `quantity` is its total and `filledQuantity` what it has executed, and `status` is its state expressed as an order status — a parent mid-execution reads `PARTIALLY_FILLED`, one that finished having executed nothing reads `EXPIRED`, and a conditional still waiting on its trigger reads `SUBMITTED`.
Existing filters (`status`, `tradingPairId`, `tradingMode`, `marginAccountId`), both pagination modes and the sort order are unchanged and apply to the merged stream. `tradingMode=SPOT` excludes conditional orders, which are margin-only. The dedicated `listTwapOrders` and `listConditionalOrders` calls are unchanged and remain the way to list one kind with its own filters.
The WebSocket `orders` channel is not affected: it still carries book orders only, and its subscribe-time snapshot has no field for either detail. See [Order queries](/sdk/typescript/trades#order-queries) and [List Orders](/developers/trade#list-orders).
### Batch replace fails per item, not per batch
`POST /api/v1/orders/batch-replace` (gRPC `BatchReplaceOrders`; `sdk.trading.batchReplace`) now scopes **every** pre-sequencing validation failure to its own item, matching batch create. A malformed `orderId`, `price`, `quantity` or `clientOrderId`, a `postOnly` violation against the persisted original, and a delegated-session item without a persisted original each fail alone — that item's result slot carries a structured `error.code` from batch create's vocabulary (`INVALID_ORDER_ID`, `INVALID_PRICE`, `INVALID_QUANTITY`, `INVALID_CLIENT_ORDER_ID`, `INVALID_POST_ONLY`, and `FORBIDDEN` for a delegated item without a persisted original) — while the batch's valid items proceed to the engine. A failed item's original order is never touched.
Previously one malformed item rejected the whole request with REST `400` / gRPC `INVALID_ARGUMENT` before anything executed. A `400` still means the batch did not execute — what changed is that a malformed item no longer produces one, so the case you have to handle now is a **`2xx` carrying per-item failures**: read `results[]` rather than inferring all-or-nothing from the top-level status. A batch whose items all fail validation returns `2xx` with every result failed and no engine round trip. Empty and oversize (more than 100 items) batches are the only input validation that still rejects the whole request; the endpoint's other top-level statuses (`401`, `429`, `500`) are unchanged.
On both batch create and batch replace, `totalSucceeded` / `totalFailed` are now derived from the merged `results[]`, so `totalRequested === totalSucceeded + totalFailed` holds on every response. An engine row that reports success without a payload is reported as an item error (`UNKNOWN_ERROR`) rather than counted as a success with no order to act on. No response shapes changed — `results[]` already carried per-item `{ code, message }` errors positionally.
Through the TypeScript SDK this is visible only for the checks the client-side schema cannot make, since its own Zod validation still rejects a malformed item before the request is sent: a `postOnly` shape violation against the persisted original, and a delegated item with no persisted original. Callers bypassing the schema see the full change. See [Batch Replace](/developers/trade#batch-replace).
### Authenticated reads draw a per-account budget
Every authenticated read now draws one request from a single per-account read budget: on REST every signed `GET` (plus the `POST` order-risk previews under `/api/v1/margin/…/simulate-order-risk`, which are reads), and on gRPC every `Get*`, `List*` and `Simulate*` RPC. The default is 30 requests per second sustained with a burst of 100. Writes are unaffected — they are metered by the existing order and movement budgets — and public, unauthenticated market data is not metered at all.
Delegated agent sessions share the owning account's read budget, because the budget is keyed to the resolved account; sub-accounts have their own. Reads return no budget headers: an over-budget read is the same contract as the other classes — REST `429` `RATE_LIMIT_EXCEEDED` with `Retry-After` and `details.retryAfter`, gRPC `RESOURCE_EXHAUSTED` with a `google.rpc.RetryInfo` detail — and the affected read operations now declare `429` in the API contract. Like the order budgets, the counters are held per API task rather than fleet-wide, so the published figure is the per-task quota.
Enforcement follows the same per-environment rollout as the order and movement budgets: until it is enabled where you trade, over-budget reads are admitted and only measured. Build the back-off handling now. Prefer the WebSocket streams for anything you would otherwise poll — order, position and balance updates arrive as events and cost no read budget. See [Rate limits](/developers/rate-limits#reads).
## Fixed
### A TWAP parent's resting slice child can no longer be cancelled directly
A `PASSIVE` TWAP parent rests one slice child on the book, and that child is a real order in the owner's book and index — so the public cancel path could reach it. Cancelling it succeeded, and that was a trap: the parent stayed live and swept the same slot as a **taker** at its deadline, so an order the trader believed cancelled came back and charged taker fees.
`POST /api/v1/orders/cancel` (the order id travels in the request body; gRPC `CancelOrder`) and the batch cancel forms now refuse a slice child with `order {orderId} is the resting slice child of TWAP parent {twapOrderId} and cannot be cancelled directly; cancel the parent instead`. Replace already refused it with the matching wording, so the parent is now the only control surface for its children on both paths.
`POST /api/v1/orders/twap/{twapOrderId}/cancel` is unchanged and remains the way to stop a TWAP — it cancels the resting child in the same atomic step. `POST /api/v1/orders/batch-cancel-all` and its by-pair form enumerate the owner's resting orders, so a slice child now appears there as one refused item naming its parent while every other order in the sweep still cancels; cancel-all does not stop TWAP parents. See [TWAP orders](/trading-mechanics/twap-orders).
### Unused execution collateral returns after a first-use isolated market order executes
Where automatic execution-collateral refunds are enabled, authenticated `MARKET` order creation through `POST /api/v1/orders` and batch order creation now release unused automatic collateral once a first-use `ISOLATED` order has executed. Any positive fill triggers it: a market order that filled only part of its quantity before the remainder was cancelled releases exactly as a fully filled one does, and the release is capped at the allocation the order originally drew. The initial execution-price and fee allowance still protects admission; settlement returns only the unused automatic allocation to the parent margin account, retaining position margin, fees, funding, losses, and the reserves of other live orders. Existing explicit collateral is untouched.
`POST /api/v1/margin/risk-buckets/simulate-order-risk` applies the same release, so projected `freeCollateralAfter` and `estimatedLiquidationPrice` reflect the final backing. Request fields are unchanged. A position's `isolatedMargin` can still sit above notional divided by leverage when the mark differs from the fill or other risk requirements consume collateral. An order that executed nothing at all releases through the ordinary cancellation-settlement rules instead.
The rollout defaults to off and is enabled per environment by an administrator through the authenticated `PUT /api/v1/feature-flags` endpoint, only once compatible verifier and proposer instances are healthy. While it is off, placement and preview retain the prior allocation behavior, and orders already recorded replay the release policy captured with them regardless of later flag changes. See [Perpetual futures](/trading-mechanics/perps).
## Upgrade
```bash theme={null}
npm install @0xmonaco/core@1.0.57 @0xmonaco/types@1.0.57 @0xmonaco/react@1.0.57 @0xmonaco/contracts@1.0.57
# or bun
bun add @0xmonaco/core@1.0.57 @0xmonaco/types@1.0.57 @0xmonaco/react@1.0.57 @0xmonaco/contracts@1.0.57
```
Three things to check before upgrading. If you encode `SweepParams` yourself, re-encode against the new leading `amount` field and add handling for `StaleQuote` and `QuoteNotBound` — the previous tuple shape is rejected. If you switch exhaustively on `order.orderType`, add the `TWAP` and `CONDITIONAL` branches; a list that assumed `GET /api/v1/orders` returned book orders only will now see parent rows. And if you call `batchReplace`, read `results[]` per item rather than inferring the outcome from the status alone — a `400` still means nothing executed, but a malformed item now comes back as a failed item inside a `2xx`.
The rest is additive or inert. `twap` and `conditional` are optional and absent on a book order. The read budget is measured rather than enforced until it is turned on per environment, so nothing that works today starts failing on upgrade — but adding `429` handling now costs nothing. The TWAP cancel change only refuses a request that previously produced a misleading success, and the execution-collateral release is off by default; when it is on it returns collateral rather than withholding it, on any positive fill.
# V1.0.58
Source: https://docs.0xmonaco.com/releases/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).
# V1.0.59
Source: https://docs.0xmonaco.com/releases/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 share 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**. `appliedBps` on a payout row is the **realized** whole-bps rate for that fill, 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.
# V1.0.60
Source: https://docs.0xmonaco.com/releases/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.
# V1.0.61
Source: https://docs.0xmonaco.com/releases/v1.0.61
# Monaco Protocol SDK v1.0.61
This release covers the consumer-facing changes since v1.0.60. A perp market now publishes its **full notional-bracket margin ladder** on `riskTiers` instead of the single synthetic tier the field used to carry, and the same ladder prices read-time margin and liquidation across the position, portfolio and margin-account endpoints — so anything that read `riskTiers[0]` as "the" tier, or derived a requirement from the top-level ratios alone, now needs to select a bracket. No field was removed or renamed, but the array's length and meaning both change and a caller that does not migrate reads tier-1 limits for a higher-notional position without any error — so it is listed as breaking. The order-risk simulations return `expectedMatchResult` — the engine's own simulated fill for the previewed size, in the same `MatchResult` shape a placement carries — plus `referencePrice`, and accept an optional `slippageToleranceBps` so the preview runs under the band the real order will. And a margin reduce-only replace may now name a close size at or below what the order has already filled, which was refused on every order before. If you read `riskTiers`, carry a previewed slippage tolerance into placement, or resize partially filled reduce-only closes, read the three upgrade notes before you bump.
## Breaking
### `riskTiers` carries the full margin ladder
`GET /api/v1/market/pairs/{trading_pair_id}/perp/config` (`sdk.market.getPerpMarketConfig`; gRPC `MarketService.GetPerpMarketConfig`, public, no auth) now lists **every** bracket in `riskTiers`, ascending by `lowerBoundNotional`. The field previously reported a single synthetic tier, so its length and its meaning both change.
Each `RiskTier` gains five fields:
| Field | Type | Meaning |
| - | - | - |
| `tierLevel` | number | 1-based bracket index; `1` is the lowest bracket |
| `lowerBoundNotional` | string | USD notional where the bracket starts; `"0"` for tier 1 |
| `maxLeverage` | string, optional | the bracket's leverage cap — absent only on a single-tier market configured from explicit margin rates |
| `initialMarginDeduction` | string | continuity term of the ladder floor; `"0"` for tier 1 |
| `maintenanceMarginDeduction` | string | continuity term of the maintenance requirement; `"0"` for tier 1 |
The deductions keep the ladder continuous where one bracket hands over to the next. For a position of notional `N` in bracket `k`:
```
IM_floor(N) = N x initialMarginRatio_k - initialMarginDeduction_k
MM(N) = N x maintenanceMarginRatio_k - maintenanceMarginDeduction_k
```
Each bracket's rates are marginal, so reading a bracket's `initialMarginRatio` as a flat rate against the whole notional overstates **that bracket's ladder floor** at the bottom of the bracket. It is not the position's initial margin in either direction: that is the greater of the floor and the selected-leverage requirement below, which at low leverage is far above the flat-rate figure.
A position's bracket is the **last** tier whose `lowerBoundNotional` is at or below its notional, `abs(quantity) x markPrice`. Its actual initial margin is the **greater** of `IM_floor(N)` and the selected-leverage initial margin scaled to the current mark — the ladder sets a floor, it does not replace the leverage you chose.
The top-level `maxLeverage`, `initialMarginRatio` and `maintenanceMarginRatio` are unchanged and remain the **tier-1** values, so a caller reading only those reads the lowest bracket rather than the position's own. `maxPositionSize` stays a market-wide maximum absolute base-asset quantity repeated on every tier, and its deprecated `maxPositionNotional` alias is unchanged — it is a base quantity, not a notional.
The same ladder now prices maintenance margin and liquidation prices on the position, portfolio and margin-account reads, so a position whose notional sits in a higher bracket reports a higher maintenance requirement and a closer liquidation price than the flat tier-1 rate would have given. Positions inside tier 1 are unaffected: its deductions are `"0"`, which leaves the arithmetic exactly as before.
See [Margin ladders](/trading-mechanics/perps#margin-ladders) for the derivation and [`getPerpMarketConfig`](/sdk/typescript/market) for the field reference.
## Added
### Order-risk previews return the engine's simulated fill
`sdk.marginAccounts.simulateOrderRisk` (`POST /api/v1/margin/accounts/{margin_account_id}/simulate-order-risk`), `simulateParentMarginOrderRisk` (`POST /api/v1/margin/parent-margin-account/simulate-order-risk`) and `simulateRiskBucketOrderRisk` (`POST /api/v1/margin/risk-buckets/simulate-order-risk`) — and their gRPC equivalents `MarginAccountsService.SimulateOrderRisk`, `SimulateParentMarginOrderRisk` and `SimulateRiskBucketOrderRisk` — now publish the match the engine already ran to admit the order. All three need an authenticated session and are owner-scoped: a preview only ever runs against the caller's own margin accounts.
`SimulateOrderRiskResponse.expectedMatchResult?: MatchResult` is the **same** shape `CreateOrderResponse.matchResult` carries for a placement (and `ReplaceOrderResponse.matchResult`, `BatchCreateResult.matchResult`, `BatchReplaceResult.matchResult`; the `Order` read model does not carry it), from the same walk over the live book: `totalFilled`, `remainingQuantity`, `averageFillPrice`, `executionPriceRange`, `status`, `actualSlippageBps` and `maxSlippageBps`. It is not a client-side re-walk, so a preflight check and the placement that follows read the same numbers for the same book — but the book moves, so treat the figures as indicative of the size's impact rather than as a guaranteed execution. `remainingQuantity` is the partial-fill indicator — it is what the band or the depth would leave unfilled on the previewed book, and the preview's `status` reads `CANCELLED` for it, because a MARKET remainder cannot rest. Placement runs against a later book, so a non-zero remainder here is a warning that the size does not clear, not a prediction of the fill: read the placement's own `status` and quantities for what actually happened.
`SimulateOrderRiskResponse.referencePrice?: string` is the touch on the taking side the MARKET preview was measured from — best ask for a BUY, best bid for a SELL — and is what `actualSlippageBps` is measured against.
Both fields are present **only when `accepted` is true**, and absent — never zeros — on every refused preview, including the post-match maker-risk rejection. Treat absence as unknown rather than as a zero fill. `referencePrice` is additionally MARKET-only: a LIMIT or IOC preview is measured against its own limit price and publishes nothing here, and its `actualSlippageBps` is `null` when every fill improved on that price.
The three requests gain an optional `slippageToleranceBps` (MARKET only, `0`–`1000`), validated client-side by the newly exported `MarketSlippageToleranceBpsSchema`: a LIMIT preview carrying the field, or a value above 1,000 bps, fails validation before the request is sent, and reaches the server as a `400` (gRPC `INVALID_ARGUMENT`) carrying placement's own message if sent raw. It tightens the previewed walk to the band the real order will run under, so `expectedMatchResult` and `estimatedFee` price under your own tolerance; the public handler previously always previewed under the default band. It can only tighten — it never widens the 1,000 bps protective band.
The preview also now runs under the **caller's wallet** rather than a placeholder, so self-trade prevention classifies your own resting orders exactly as placement does.
These figures price against the book **at the moment of the preview**. A take-profit or stop-loss closes later, on a different book and under the wider band a triggered market leg carries, anchored at trigger time — so size a TP/SL target by applying `expectedMatchResult.actualSlippageBps` to your **trigger** price as an impact figure, rather than displaying this call's absolute `averageFillPrice`.
A preview cannot carry a post-only flag, TP/SL legs or a per-order self-trade-prevention override, so a post-only order that would cross is refused by placement, not by the preview.
On the MCP server, `simulate_parent_margin_order_risk` and `simulate_risk_bucket_order_risk` accept the same input and surface both response fields. The MCP server ships at `1.0.61` too; it runs via `npx -y @0xmonaco/mcp-server`, so restarting your assistant picks up the new tool schema.
See [Pre-trade simulation](/sdk/typescript/margin-accounts#pre-trade-simulation).
## Fixed
### A margin reduce-only replace can be sized down below its filled amount
`sdk.trading.replaceOrder` (`PUT /api/v1/orders/{orderId}`) and each item of `sdk.trading.batchReplace` (`POST /api/v1/orders/batch-replace`; gRPC `OrdersService.ReplaceOrder` and `BatchReplaceOrders`) rejected `quantity` at or below the order's `filledQuantity` on **every** order, with `New quantity N must be greater than filled quantity 3`.
That rule belongs to **total** semantics: a replacement quantity is the order's new total, and a total at or below what already filled leaves nothing to rest. A margin reduce-only quantity is deliberately not a total — it is a close size measured against the live position and placed exactly as given — so the rule never applied to it. Applying it anyway made a partially filled close unshrinkable: on a reduce-only close of 8 with 3 filled, both `quantity: "2"` and `quantity: "3"` were refused, leaving no way to reprice the close downward except by naming a size above the order's own fill count.
Both are now accepted and placed at the requested close size. As with any replacement, success does not mean the order is *resting*: a reduce-only close still goes through matching and self-trade prevention, so it can fill immediately, or be cancelled outright under `CANCEL_TAKER` / `CANCEL_BOTH` or as a `SKIP` remainder crossing one of your surviving orders — read the new order's status rather than inferring it. The replacement still inherits the original's fill history, so the row reads `quantity = filled + remaining`.
Nothing else changes. Total semantics, the `total - filled` subtraction and the must-exceed-filled rejection all behave exactly as before for every order that is not margin reduce-only, and the reduce-only position bound still rejects a close larger than the live position. No request or response shape changed — this is a server-side correction, so clients on any SDK version receive it; only the `quantity` field descriptions on `ReplaceOrderRequest` and `BatchReplaceOrderItem` now state the exception.
See [Replacing Orders](/developers/trade#replace-orders).
## Upgrade
```bash theme={null}
npm install @0xmonaco/core@1.0.61 @0xmonaco/types@1.0.61 @0xmonaco/react@1.0.61
# or bun
bun add @0xmonaco/core@1.0.61 @0xmonaco/types@1.0.61 @0xmonaco/react@1.0.61
```
Three things to check before upgrading. If you **read `riskTiers`**, stop treating it as one row: select the last tier whose `lowerBoundNotional` is at or below `abs(quantity) x markPrice`, take the position's initial margin as the greater of that bracket's ladder floor and your selected-leverage requirement, and handle `maxLeverage` being absent on a single-tier market. The top-level ratios are tier 1, not the position's tier. If you **carry a previewed slippage tolerance into placement**, note the units differ: previews take `slippageToleranceBps` as integer basis points, while `placeMarketOrder` and the batch helpers take `slippageTolerance` as a **ratio**, so pass `slippageToleranceBps / 10_000` — a `slippageToleranceBps` key on those calls is not forwarded. And if you **resize partially filled reduce-only closes**, the `400` you were coding around is gone; a close size at or below the fill count is now a legitimate size-down, so drop any client-side floor that was compensating for it.
The rest is additive. `expectedMatchResult` and `referencePrice` are present only on an accepted preview, so read them behind an `accepted` check rather than assuming a shape. Existing preview calls that omit `slippageToleranceBps` keep the default band and are unchanged, except that the walk now runs under your own wallet for self-trade prevention.
# V1.0.62
Source: https://docs.0xmonaco.com/releases/v1.0.62
# Monaco Protocol SDK v1.0.62
This release covers the consumer-facing changes since v1.0.61. The `account` WebSocket channel now delivers a **subscribe-time snapshot** — one row per margin account you own in the authenticated application, flat accounts included — so an immediate baseline no longer needs a margin-accounts REST call; to describe those snapshot rows, `AccountEventData.sequence` is now optional, which is source-breaking for strict TypeScript. PitPass gains two self-scoped referral reads, `getMyReferralPosition` and `getMyReferralDownline`, with matching React hooks and gRPC RPCs, and the verify response now reports `isNewUser` and `referralApplied`. The `market_stats` channels pick up the same snapshot-on-subscribe treatment, and the server-key application order and movement listings now bound `total` / `totalPages` at the depth they can page to. If you consume the `account` channel under strict TypeScript, or read `total` on the application listings as a lifetime count, read the two upgrade notes before you bump.
## Breaking
### The `account` channel snapshot makes `AccountEventData.sequence` optional
`sdk.ws.account(handler, onSnapshot)` (authenticated session; raw channel `account`) now delivers a **subscribe-time snapshot** ahead of the live diffs: one `AccountEventData` row per margin account you own **in the application you authenticated under**, delivered as a single array through the `onSnapshot` callback. No SDK migration is needed — the `onSnapshot` parameter already existed on this subscription — but the baseline reaches only subscriptions that pass an `onSnapshot` handler, so add one if you don't already. Owning no margin account then delivers an empty array rather than a skipped call.
**FLAT accounts are included.** The live producer only emits for an account holding an open position or a resting order reserve, so a flat account was silent on the stream until it re-activated; the snapshot carries it with its current persisted health — a zero maintenance requirement and a zero margin ratio. A flat account that is also otherwise empty (no open risk bucket) reports its deposited collateral as both `equity` and `freeCollateral`; a flat account still holding an open risk bucket keeps that risk bucket's allocation and unsettled realized PnL, so its `equity` need not equal its `collateral`.
Because a snapshot row is the same payload as a live frame with `sequence` **omitted**, the parser reads the field optionally and **`AccountEventData.sequence` is now `number | undefined`**. This is SOURCE-BREAKING for strict TypeScript consumers: `event.data.sequence.toFixed()`, or passing it where a `number` is required, no longer compiles. Guard it (`event.data.sequence ?? fallback`, or narrow with a check) and treat absent as **unknown, never zero**, and never as a replay or ordering cursor — the counter is the matching engine producer's process-local per-account map, so a zero from any other emitter is indistinguishable from a restarted producer's first frame, and it resets on a producer restart. Runtime behaviour is unchanged: live frames still always carry a sequence, and a malformed value on a live frame is still rejected.
Merge the snapshot **per `marginAccountId`** rather than replacing your whole channel map, and let any live frame supersede a snapshot row for the same account whatever the timestamps say: a snapshot row's `updatedAt` is when its state was persisted (a database write), while a live frame's is when the engine sampled it, so the two clocks are not comparable under persistence lag. Rank `updatedAt` only between live frames — newest wins, with `sequence` breaking ties inside one producer run. Live delivery is also per user rather than per application, so a wallet trading through more than one application still receives live frames for its other accounts; each is full current health and baselines itself.
See [Subscribe-time snapshots](/sdk/typescript/websockets#subscribe-time-snapshots) and the [raw `account` channel reference](/reference/websockets#account).
## Added
### PitPass self-scoped referral reads
`sdk.pitpass.getMyReferralPosition()` (`GET /api/v1/pitpass/referrals/me`) and `sdk.pitpass.getMyReferralDownline()` (`GET /api/v1/pitpass/referrals/me/downline`) — with the gRPC equivalents `TraderCodeService.GetMyReferralPosition` and `TraderCodeService.GetMyReferralDownline` — return the caller's own referral chain. Both need an authenticated session and are **self-scoped**: identity is derived from the session, there is no id path parameter, so a request cannot read another user's chain. Every other user is exposed only as a **public wallet address** plus a custom-handle-or-wallet `display`, never an internal id.
`getMyReferralPosition` returns `GetMyReferralPositionResponse`: your direct (L1) `referrer` and `referrerDisplay` (both empty when you were not referred — only the direct referrer is exposed), the all-time `totalEarned` across every level and token, and `rewards`, your most recent `ReferralRewardEntry` rows newest-first (up to the 100 newest — `totalEarned` stays the complete aggregate — each carrying the reward's `source`, `level` 1/2/3, `amount`, `token`, `rateApplied` and `createdAt`).
`getMyReferralDownline` returns `GetMyReferralDownlineResponse`: `earningsBySource`, one `ReferralEarningsSource` row per (source trader, level, token) you earn from across L1/L2/L3; `directReferees`, your L1 roster as `ReferralDirectReferee` rows **including referees who have not traded yet** (their `totalEarned` reads `"0"`), showing up to the 500 most recent effective referees (after per-wallet dedupe and the winning-edge filter), drawn from a bounded scan of recent relationships — so a referrer with a very large history may get an incomplete roster — while the `earningsBySource` and `summary` earnings views stay unbounded and count every level, so a caller past that roster cap still reads their full earnings; and `summary`, a `ReferralEarningsSummary` carrying `totalEarned` and the per-level `level1Earned` / `level2Earned` / `level3Earned` — always populated, so a brand-new user reads a zeroed summary rather than an absent field. All amounts are RAW atomic units of the token.
`@0xmonaco/react` wraps both as the `useMyReferralPosition` and `useMyReferralDownline` hooks.
See [PitPass](/sdk/typescript/pitpass) and [the React PitPass hooks](/sdk/react/hooks/use-pitpass).
### The verify response reports `isNewUser` and `referralApplied`
`AuthState` — returned by `sdk.auth.authenticate` and `sdk.auth.verifySignature` (`POST /api/v1/auth/verify`) — now carries two optional flags read from the verify response. `isNewUser` is `true` when this verify created the user (the wallet's first sign-in to this application); `referralApplied` is `true` when a PitPass referral relationship was recorded on this verify (a valid TraderCode was applied). Gate first-time UX on `isNewUser`, and confirm a referral landed with `referralApplied`. Both are optional, so read them behind a presence check.
See [Authentication](/sdk/typescript/auth).
## Changed
### The `market_stats` channels deliver a subscribe-time snapshot
`sdk.ws.marketStats(pairId, handler, onSnapshot)` and `sdk.ws.marketStatsAll(handler, onSnapshot)` (public, no auth) now receive a subscribe-time snapshot ahead of the live frames. The per-market form delivers a one-element `MarketStatsData[]` carrying that pair's latest complete stats frame; the all-markets form delivers a `MarketStatsSummary[]` with one summary per active market with retained data, ordered by trading-pair id. Each per-market row is the same shape as the live per-market payload, and each all-markets summary is the same shape as an element of the live all-markets frame's `markets` list — so apply it as a terminal-state baseline and let the live frames move it forward. No SDK migration is needed — the `onSnapshot` parameters already existed — but the baseline reaches only subscriptions that pass an `onSnapshot` handler, so add one if you don't already.
A per-market subscription whose market has not yet produced a frame — or on a transient matching-engine read failure — receives a `SNAPSHOT_UNAVAILABLE` signal rather than a fabricated one; the subscription stays active and a later published live frame seeds it once the market produces one (a market that never publishes, e.g. a perp with no open-interest frame, stays unseeded). The all-markets channel returns `SNAPSHOT_UNAVAILABLE` the same way for a brief window during initial startup or after a server restart (until the producer commits its first full generation), and on any transient snapshot read failure against the matching engine — treat it as a resync signal and take the next periodic push as the baseline. Where a deployment does not run the market-stats snapshot service, neither channel sends a snapshot at all and the first live push seeds the baseline, so do not block on `onSnapshot` firing.
See [Subscribe-time snapshots](/sdk/typescript/websockets#subscribe-time-snapshots).
### Application listings bound `total` and `totalPages` to their pagination reach
`client.applications.listApplicationOrders()` and `client.applications.listApplicationMovements()` (`GET /api/v1/applications/orders` and `/api/v1/applications/movements`, server-key auth; gRPC `ApplicationsService.ListAppOrders` / `ListAppMovements`) now bound their `total` and `totalPages` by the listing's pagination reach. Both stay **exact** up to 10,000 pages of the requested `pageSize` (`pageSize` × 10,000 matching rows) and saturate there, so an application with more matching orders or movements than the listing can page through reads the cap — `totalPages` becomes `10000`, the last page it can reach — instead of its lifetime total. Applications under that reach see the same numbers as before.
The bound lets the server stop counting at the cap rather than walking the whole application's history on every page render. Loops that stop at `page >= totalPages` keep terminating; only code that read `total` as the application's lifetime count needs to treat exactly `pageSize` × 10,000 as "at least this many". On the same two endpoints, a `page` above 10,000 is now rejected as invalid input — HTTP `400` (error `BAD_REQUEST`) on REST, `INVALID_ARGUMENT` on gRPC — instead of fetching rows past the reported last page.
See [Application Backend Endpoints](/api-reference).
## Upgrade
```bash theme={null}
npm install @0xmonaco/core@1.0.62 @0xmonaco/types@1.0.62 @0xmonaco/react@1.0.62
# or bun
bun add @0xmonaco/core@1.0.62 @0xmonaco/types@1.0.62 @0xmonaco/react@1.0.62
```
Two things to check before upgrading. If you **consume the `account` channel under strict TypeScript**, `AccountEventData.sequence` is now `number | undefined`: guard every read (`event.data.sequence ?? fallback`) and treat absent as unknown rather than zero — snapshot rows omit it, live frames always carry it. And if you **read `total` on the application listings** as a lifetime count, it is now capped at `pageSize` × 10,000; treat exactly that value as "at least this many", and expect a `page` above 10,000 to be rejected.
The rest is additive. The `account` and `market_stats` subscribe-time snapshots arrive through the optional `onSnapshot` callback, so a subscription that already passes one starts receiving a baseline with no code change — add the callback if you don't already; merge the `account` snapshot per `marginAccountId` and let live frames supersede it. The PitPass referral reads and the new `isNewUser` / `referralApplied` flags are additive surfaces — read the flags behind a presence check.
# V1.0.64
Source: https://docs.0xmonaco.com/releases/v1.0.64
# Monaco Protocol SDK v1.0.64
This release rolls up the consumer-facing changes since v1.0.62. Order history — `sdk.trading.getPaginatedOrders` — now always returns an item count in **both** pagination modes, bounded by a server ceiling so a large history never drives an unbounded count, and the response carries a new `totalCapped` flag that tells you when that count is a lower bound. The response **schema** is additive — `totalCapped` is new and nothing is removed — but the count **values** change in two cases: cursor (`pageToken`) mode now populates `total` and `totalPages` — a count over full history, hot and archived rows merged — where it previously returned `0`, and a page-number caller with more than the ceiling of matching orders in its recent-history window now reads the capped ceiling instead of the exact count of that window. Check `totalCapped` to tell an exact count from a saturated one. v1.0.63 shipped no consumer-facing changes.
## Added
### Order history returns a bounded item count in both pagination modes
`sdk.trading.getPaginatedOrders` (`GET /api/v1/orders`, authenticated; gRPC `OrdersService.ListOrders`) now populates `total` and `totalPages` in **both** pagination modes — legacy page-number and cursor (`pageToken`) alike — where cursor mode previously returned `0` for both and offered no exact total. The count is **bounded by a server ceiling of 10,000**: it stays exact up to that many matching orders and saturates there, so a wallet with a very large order history no longer triggers an unbounded `COUNT` over the orders table on every page render. `pageSize` up to 1000 in cursor mode is unchanged.
`GetPaginatedOrdersResponse` gains `totalCapped` (`boolean`): `true` when **more than** the ceiling matched, so `total` and `totalPages` are a **lower bound** — at least that many orders exist — rather than an exact figure; `false` when `total` is exact, including exactly the ceiling, which is still reported exactly. `total` and `totalPages` are **no longer deprecated** now that both modes populate them; only `page` stays deprecated and still comes back as `0` in cursor mode.
Neither pagination loop breaks: cursor walks still terminate on an empty `nextPageToken`, and page-number loops that stop at `page >= totalPages` keep terminating. The count values are not identical to before, though: in cursor mode `total` and `totalPages` (a count over full history, hot and archived merged) were `0` and are now populated, and a page-number caller with more than 10,000 orders in its recent-history window now reads the capped ceiling rather than the exact count of that window (a page-number caller under the ceiling still reads the same exact `total`). Code that reads `total` as an exact figure should treat a capped value as "at least this many" and check `totalCapped` to tell an exact count from a saturated one.
See [Order history](/sdk/typescript/trades) and [the API reference](/api-reference).
## Upgrade
```bash theme={null}
npm install @0xmonaco/core@1.0.64 @0xmonaco/types@1.0.64 @0xmonaco/react@1.0.64
# or bun
bun add @0xmonaco/core@1.0.64 @0xmonaco/types@1.0.64 @0xmonaco/react@1.0.64
```
The response schema is additive, so there is no signature change to adopt. But order-history counts now behave differently in two cases — cursor mode populates `total`/`totalPages` where it returned `0`, and a page-number count above 10,000 saturates at the ceiling — so if you read `total` as an exact count, treat a capped value as "at least this many" and read the new `totalCapped` flag to tell an exact count from a saturated one.
# V1.0.65
Source: https://docs.0xmonaco.com/releases/v1.0.65
# Monaco Protocol SDK v1.0.65
This release bounds the page-number item counts on seven more listing reads and fills two WebSocket gaps. `sdk.profile.getUserTrades`, `sdk.positions.listPositions` / `listPositionHistory`, the margin-account movement reads, and `sdk.trading.listConditionalOrders` / `listTwapOrders` now cap the legacy page-number `total` (and `getUserTrades`'s `totalPages`) at each listing's **pagination reach** — exact up to `pageSize` × 10,000 rows and saturating there — so a caller with more history than page-number mode can page through reads the cap instead of driving an unbounded count, and `page` above 10,000 is now rejected. This is the **saturating** shape (no new field, unlike v1.0.64's `totalCapped` on order history): the response schema is unchanged, only the count **values** move, and only for callers above the reach. Of the seven, only `getUserTrades` also offers cursor (`pageToken`) pagination — its SDK default, unchanged and still reporting no totals; the other six are page-number-only (`page` / `pageSize`). On the socket, `sdk.ws.twapOrders` now delivers a subscribe-time snapshot the way the other stateful channels do, and `sdk.ws.instruments` now carries each market's `baseIconUrl` / `quoteIconUrl`. The instrument icons ride the existing frame handler; the TWAP snapshot — like every subscribe-time baseline — arrives through the optional `onSnapshot` callback, so a subscription already passing one receives it with no code change and one that omits it should add an `onSnapshot` handler to opt in.
## Changed
### Page-number listing counts are bounded by their pagination reach
Seven more listing reads now bound the legacy page-number count instead of counting a caller's whole matching history on every page render:
* `sdk.profile.getUserTrades` (`GET /api/v1/accounts/trades`; gRPC `AccountsService.GetUserTrades` and `TradesService.ListUserTrades`)
* `sdk.positions.listPositions` (`GET /api/v1/positions`) and `sdk.positions.listPositionHistory` (`GET /api/v1/positions/history`)
* `sdk.marginAccounts.getMarginAccountMovements` (`GET /api/v1/margin/accounts/{marginAccountId}/movements`) and `sdk.marginAccounts.getParentMarginAccountMovements` (`GET /api/v1/margin/parent-margin-account/movements`)
* `sdk.trading.listConditionalOrders` (`GET /api/v1/orders/conditional`) and `sdk.trading.listTwapOrders` (`GET /api/v1/orders/twap`)
In legacy page-number mode the response `total` is now **exact up to `pageSize` × 10,000 matching rows and saturates there**; `getUserTrades` bounds `totalPages` the same way (exact up to 10,000, the last page page-number mode can reach). A caller with more matching rows than page-number mode can page through reads the capped figure rather than its lifetime total. `page` is capped at 10,000: a larger page is rejected as invalid input (`page must be <= 10000`) instead of fetching rows past the reported last page.
`getUserTrades` is the only one of the seven with a cursor (`pageToken`) mode — its SDK default, unchanged: it still walks full history (hot and archived merged) and reports no totals (`total` / `totalPages` / `page` are `0`), and its page-number totals stay deprecated in favour of that cursor; build "load more" UIs on its `nextPageToken`. The other six are page-number-only — `page` / `pageSize`, no `pageToken` — and `total` is simply the count they report (it is not deprecated). The response **schema does not change** on any of them — every field keeps its type — so there is nothing to adopt. The count **values** change only for a page-number caller above the reach: it now reads the saturated cap where it previously read an exact lifetime count. A page-number loop that stops at `page >= totalPages` keeps terminating; only code that reads `total` as an exact lifetime figure should treat a saturated value as "at least this many".
See [Trade history](/sdk/typescript/trades), [Positions](/sdk/typescript/positions), [Margin accounts](/sdk/typescript/margin-accounts), and [the API reference](/api-reference).
## Fixed
### The `twapOrders` channel delivers a subscribe-time snapshot
`sdk.ws.twapOrders(handler, tradingPairId?, onSnapshot)` now receives a subscribe-time snapshot ahead of the live frames, the way the other stateful channels do: one `TwapOrderEventData` row per owned **non-terminal** (`PENDING` / `ACTIVE`) TWAP parent, optionally scoped to the subscription's trading pair, each row the same shape as a live `twap_order_update` with `reason: "snapshot"`. Previously the server sent no TWAP snapshot, so the `onSnapshot` callback was inert and a baseline needed a REST read. The SDK plumbing was already in place, so the baseline reaches only subscriptions that pass an `onSnapshot` handler: a caller already passing one receives it with no code change, and a caller subscribing as `sdk.ws.twapOrders(handler)` should add an `onSnapshot` handler to opt in. An empty baseline arrives as `onSnapshot([])`; if the read is unavailable the subscription surfaces `SNAPSHOT_UNAVAILABLE` and stays active while live diffs keep flowing.
### The `instruments` channel carries base and quote asset icon URLs
`sdk.ws.instruments(handler, tradingPairId?, onSnapshot)` now surfaces `baseIconUrl` and `quoteIconUrl` (`string | null`) on each parsed `InstrumentEventData`, on both the subscribe-time snapshot and the live frames. The server sends each key present on every market (spot and perp) — an explicit `null` when that asset has no configured icon, never a synthesized URL — and the parser reads it tolerantly, mapping a missing key to `null` so a frame is never dropped for want of it. `InstrumentEventData` gains the two fields as optional (`baseIconUrl?` / `quoteIconUrl?`), so the addition is non-breaking for code that constructs or mocks the shape. A client can now join a position's `tradingPairId` to the instrument row and render both icons without a REST read.
## Upgrade
```bash theme={null}
npm install @0xmonaco/core@1.0.65 @0xmonaco/types@1.0.65 @0xmonaco/react@1.0.65
# or bun
bun add @0xmonaco/core@1.0.65 @0xmonaco/types@1.0.65 @0xmonaco/react@1.0.65
```
The response schemas are additive-safe — no signature changes to adopt. Two behaviours differ: a page-number caller with more than `pageSize` × 10,000 matching rows on the seven bounded listings now reads a saturated `total` (treat it as "at least this many"), and the `twapOrders` and `instruments` WebSocket channels now deliver data — a TWAP snapshot and asset icon URLs — that clients previously fetched over REST.
# V1.0.66
Source: https://docs.0xmonaco.com/releases/v1.0.66
# Monaco Protocol SDK v1.0.66
This release adds **trailing stops** for perp positions and fixes two WebSocket client gaps. A trailing stop is a reduce-only market close whose trigger follows the best mark reached since it armed: attach one to an open position with `sdk.positions.attachPositionTpSl(positionId, { trailingStop })`, or to a margin entry order with `trailingStop` on `placeLimitOrder` / `placeMarketOrder`. Position TP/SL attach now draws from the order-creation rate-limit budget, and `Position.version` now advances only on a position whose content actually changed. On the socket, the client stops reconnecting in a loop after the server ends a connection for lost authentication — it calls the new `onReauthenticationRequired` option instead — and `onError` now names the `channel` a per-channel `Error` frame concerns. **Action items:** pass `onReauthenticationRequired` if your app holds authenticated WebSocket subscriptions, so it can supply a fresh session when one is revoked or expires; and if you attach TP/SL at high frequency, budget each attach as one order-creation item.
## Added
### Trailing stops
`sdk.positions.attachPositionTpSl` (`POST /api/v1/positions/{position_id}/tp-sl`; gRPC `PositionsService.AttachPositionTpSl`) accepts a `trailingStop: TrailingStopLeg`, alone or alongside `takeProfit` / `stopLoss`, and the response gains `trailingStopOrderId`. `sdk.trading.placeLimitOrder` / `placeMarketOrder` (`POST /api/v1/orders`; gRPC `OrdersService.CreateOrder`) accept the same leg as `options.trailingStop` (`ParentTrailingStopLegParams`) on margin entry orders that are not reduce-only, and `CreateOrderResponse` gains `trailingStopOrderId` — present only when the trailing stop materializes: the order fully fills on arrival or leaves a resting remainder. A partial fill whose remainder is cancelled (an `IOC` or `MARKET` order) creates no trailing stop and omits the id.
The leg takes:
* `trailBps` — integer, 10 to 2000 basis points behind the watermark
* `activationPrice?` — the mark level that starts tracking and seeds the watermark; rejected if the mark has already reached it. Omitted, tracking starts at the current mark
* `quantity?` / `closePosition?` — the same sizing contract as a TP/SL leg, mutually exclusive
* `slippageToleranceBps?` — 0 to 10000
* `expiresAt?` — ISO 8601
For a long, trigger = watermark × (1 − trailBps/10000) and it fires when the mark falls to or below it; a short mirrors it. The watermark is the best mark since arming, so the trigger only ever moves in the position's favour. It fires through the standard conditional market close (1,200 bps band, optional slippage cap).
* **One per position.** A second attach is rejected with `409` (gRPC `ALREADY_EXISTS`) — as is an attach while a resting entry order on the same trading pair and risk bucket carries a pending trailing stop — never replaced, and a trailing stop never supersedes or is superseded by a full-close TP/SL leg. It coexists with fixed TP/SL and never joins `oco`.
* **No modify.** Cancel it and attach a new one.
* **Entry-order attach** arms only when the order **fully** fills, and is cancelled with the order otherwise.
* **Reads.** `ConditionalOrder` (`getConditionalOrder`, `listConditionalOrders`, and the conditional detail of order history) reads `conditionType: "TRAILING_STOP"` and gains `trailBps`, `activationPrice`, `watermarkPrice` and `trailArmedAt`. An entry-attached stop waits as `PENDING_PARENT` until its order fully fills; from then on (and from the start for a position attach) the row stays `state: "ACTIVE"` while waiting for activation and while tracking; `trailArmedAt` is absent until tracking begins, and `triggerPrice` is provisional until the first mark after arming.
* **Streaming.** `sdk.ws.conditionalOrders` frames (snapshot and live) carry the same four fields, read leniently. Live frames carry `reason: "armed"` when tracking begins and `reason: "ratcheted"` on every trigger move, with the new `triggerPrice` and `watermarkPrice`.
The SDK rejects a `trailBps` outside 10–2000 or not an integer, `quantity` together with `closePosition`, and a trailing stop on a SPOT or reduce-only order before any request is made. `@0xmonaco/types` adds `TrailingStopLeg`, `ParentTrailingStopLegParams`, `"TRAILING_STOP"` on `ConditionalOrderConditionType`, `"armed"` / `"ratcheted"` on `ConditionalOrderEventReason`, and the matching Zod schemas.
See [Trailing Stop](/developers/trade#trailing-stop) and [Positions](/sdk/typescript/positions).
## Changed
### Position TP/SL attach is metered by the order-creation rate limit
`attachPositionTpSl` (`POST /api/v1/positions/{positionId}/tp-sl`; gRPC `PositionsService.AttachPositionTpSl`) now draws **one** item per call from the order-creation budget it shares with order creates, replaces, batch items and TWAP creation — whatever legs the call carries. The account-family tier, the PrivateLink column and the `X-RateLimit-*` headers apply exactly as on the order endpoints. Over budget it returns `429` `RATE_LIMIT_EXCEEDED` with `Retry-After` on REST and `RESOURCE_EXHAUSTED` with `RetryInfo` on gRPC. Position closes stay unmetered. Like the other order budgets, enforcement is warn-only until it is turned on for the environment, so handle `429` / `RESOURCE_EXHAUSTED` now and back off on the server's retry hint rather than waiting for a rejection. See [Rate Limits](/developers/rate-limits).
### `Position.version` advances only when the position changed
`version` on position reads now advances exactly when that row's versioned content changed at a sequence (including a change undone within one persistence batch), instead of on every open position of the margin account whenever a sequenced command touched it. An unchanged position keeps its `version`. A ranked live `position_update` frame for an untouched sibling of a filled position can therefore carry a higher version than the REST row while describing identical content — rank it as newer; nothing changes. The per-field-group merge rule is unchanged, and there is no field or type change. See [Positions](/developers/positions).
## Fixed
### The WebSocket client stops reconnecting after authentication is lost
After a `SESSION_INVALID` `Error` frame followed by a `1008` close (the session was revoked or expired), or an `AUTH_FAILED` frame after the connection sent `Authenticate`, the client now stops reconnecting instead of retrying in a loop. It drops the rejected session keypair, keeps your subscription handlers, reports `"disconnected"`, and calls the new `onReauthenticationRequired({ code, message, closeCode })` option once. Supplying a fresh session (`sdk.login`, or `setSessionKeypair` on a `createMonacoWebSocket` client) reconnects, authenticates, resubscribes every channel you still hold, and fires `onResync`. Every other abnormal close — including a `1008` for the inbound message-rate cap and a `1013` slow-client shed — keeps the backoff reconnect. `@0xmonaco/types` exports `ReauthenticationRequiredHandler` and `WebSocketReauthenticationInfo`. See [Session loss](/sdk/typescript/websockets#session-loss-close-code-1008).
### `onError` names the channel a per-channel failure concerns
`onError` now delivers `{ code, message, channel }` for a per-channel `Error` frame (`INVALID_SUBSCRIPTION`, `AUTH_REQUIRED`, `SUBSCRIPTION_LIMIT`, `SNAPSHOT_UNAVAILABLE`), with `channel` echoed exactly as you sent it — so an app can tell which channel of a batched or reconnect `Subscribe` failed, and refetch only that one, without matching the human-readable `message`. A connection-wide failure is still delivered as exactly `{ code, message }`. `WebSocketErrorInfo` gains the optional `channel?: string`. See [Server error frames](/sdk/typescript/websockets#server-error-frames).
## Upgrade
```bash theme={null}
npm install @0xmonaco/core@1.0.66 @0xmonaco/types@1.0.66 @0xmonaco/react@1.0.66
# or bun
bun add @0xmonaco/core@1.0.66 @0xmonaco/types@1.0.66 @0xmonaco/react@1.0.66
```
Every change is additive for TypeScript callers — no signature changes to adopt. Two behaviours differ: an authenticated WebSocket connection whose session is revoked or expired now stays disconnected until you supply a fresh session (handle `onReauthenticationRequired`), and each `attachPositionTpSl` call now counts against the order-creation budget.
# V1.0.67
Source: https://docs.0xmonaco.com/releases/v1.0.67
# Monaco Protocol SDK v1.0.67
This release separates an isolated position's **committed backing** from the funds held for its unfilled orders: `isolatedMargin` and `liquidationPrice` on position reads now reflect only what the filled position has committed, and `riskBucketCollateral` on an order funds order capacity rather than liquidation protection. The SDK's conditional-order and TWAP listings move to **cursor pagination** by default, the market screener gains **open-interest averages**, and the `liquidations` channel gains a **subscribe-time snapshot** and a **terminal alert when an episode recovers**. The SDK also corrects for a skewed device clock when it signs requests, `conditionalOrders` frames carry `createdAt`, batch-cancel responses report the engine's millisecond cancel stamp, each funding premium sample is bounded before it is averaged, and the WebSocket limits are raised. **Action items:** if you read `isolatedMargin` or `liquidationPrice`, expect a tighter value for an isolated position with large resting orders; if you read `total` / `page` from `listConditionalOrders` or `listTwapOrders`, follow `nextPageToken` instead or pass `page` explicitly; and if you consume `ws.liquidations`, treat a `COMPLETED` alert as the end of the episode — it closes a recovered episode as well as one liquidated to completion, so it is not by itself a sign that anything was liquidated.
## Changed
### Isolated position backing excludes open-order funds
Isolated positions are now backed by filled-position backing: committed principal, realized PnL, signed trading fees (a taker fee reduces it, a maker rebate adds to it) and funding. `isolatedMargin` on `GET /api/v1/positions` and `GET /api/v1/positions/{position_id}` (gRPC `PositionsService.ListPositions`, `GetPosition`) reports that backing, and `liquidationPrice` on those two and on `GET /api/v1/positions/{position_id}/risk` (gRPC `GetPositionRisk`, which carries no `isolatedMargin`) is computed from it. Funds reserved or parked for unfilled orders are excluded, and live and persisted reads agree. Previously a small partial fill could inherit protection from its whole order's risk bucket and show a liquidation price far from the mark. Field names and wire types are unchanged.
`riskBucketCollateral` on `POST /api/v1/orders` and `POST /api/v1/orders/batch-create` (gRPC `OrdersService.CreateOrder`, `BatchCreateOrders`) still seeds a new isolated risk bucket, but the amount is an order budget: fills commit their required margin and paid-fee funding to the position's backing, and unfilled or unused order funds do not protect it from liquidation. An automatically funded isolated `LIMIT` order returns unused taker-fee funding once its execution and resting-order fee liabilities are known. Explicit position-margin additions stay committed. See [Positions](/developers/positions) and [Trade](/developers/trade).
### Conditional-order and TWAP listings gain cursor pagination
`GET /api/v1/orders/conditional` and `GET /api/v1/orders/twap` (gRPC `OrdersService.ListConditionalOrders` and `ListTwapOrders`) gain keyset cursor pagination: send `pageToken` (`page_token` on gRPC) — `""` to start — to walk `(createdAt, id)` newest-first, and follow each response's `nextPageToken` until it comes back empty, with a `pageSize` up to 1000. The server selects cursor mode only when `pageToken` is present, so a raw request that omits both `pageToken` and `page` still gets page-number mode; send an explicit empty `pageToken` to start a walk. `sdk.trading.listConditionalOrders` and `sdk.trading.listTwapOrders` default to cursor mode: omitting `page` (or passing `pageToken`) sends the empty token for you. Cursor responses return `total` and `page` as `0` and run no count, and a token pins its request filters — replaying it against different filters is rejected. Passing the deprecated `page` still selects page-number mode, with its bounded `total` and 100 page-size cap. `@0xmonaco/types` adds `pageToken` to `ListConditionalOrdersParams` and `ListTwapOrdersParams` and `nextPageToken` to both responses. See [Trades](/sdk/typescript/trades).
## Added
### Screener open-interest averages
`sdk.market.getScreener()` (`GET /api/v1/market/screener`; gRPC `MarketService.GetScreener`) rows gain nullable decimal-string `openInterestNotionalAvg1h`, `openInterestNotionalAvg24h` and `openInterestNotionalAvg7d`, and each `ScreenerSnapshotPoint` gains `openInterestNotionalAvg`. Each is the mean of one valued gross open-interest notional per completed UTC minute, at the latest trade price, over 60, 1440 or 10080 minutes or a completed UTC day. A value is published once at least 95% of the window's minutes carry a sample (57, 1368 or 9576; 1368 for a daily point). Spot pairs and under-covered windows are `null`, and a known zero is `"0"`. There is no historical backfill, so the windows fill in after deployment. See [Market Screener](/sdk/typescript/market#market-screener).
### `createdAt` on conditional-order frames
`sdk.ws.conditionalOrders()` live `conditional_order_update` frames and subscribe-time snapshot rows carry `createdAt`, the same ISO 8601 string REST returns for that conditional order. `ConditionalOrderEventData.createdAt` is optional only so a frame from an older deployment during a rolling release still parses. See [WebSockets](/sdk/typescript/websockets).
## Fixed
### The liquidations channel has a baseline and reports recoveries
`sdk.ws.liquidations(handler, tradingPairId?, onSnapshot)` now receives a subscribe-time snapshot: one `LiquidationEventData` row per in-flight (`UNRESOLVED`) liquidation record you own, with `accountState: "Liquidating"`. A record is account-scoped, so each row's `positionId`, `tradingPairId`, `liquidationPrice` and `markPrice` are absent (`null` on the raw wire; the SDK parses them to `undefined`), and a pair-scoped `liquidations:{pair}` subscription baselines empty — subscribe to the bare `liquidations` channel for the account-wide baseline. An empty baseline arrives as `onSnapshot([])`; an unreadable read returns `SNAPSHOT_UNAVAILABLE` and leaves the subscription active.
A liquidation episode that recovers — its risk bucket heals back over maintenance on a liquidation step, an oracle mark, a funding settlement, or an auto-deleveraging close that lifts it — now closes its record as `COMPLETED` and sends one terminal `liquidation_alert` with `status: "COMPLETED"`, `accountState: "Normal"` and the episode's `liquidationId`. Previously a recovered episode ended silently and its record stayed `UNRESOLVED`. See [Liquidations](/reference/websockets#liquidations).
### Signing survives a skewed device clock
Signed REST requests and the WebSocket `Authenticate` handshake no longer fail with `401` `Request timestamp outside allowed skew` when the device clock is more than 30 seconds off. The SDK reads the server's time from each response's `Date` header, signs with the corrected time once the offset reaches 5 seconds, and retries a signed request once after a `401` that corrected the clock. A WebSocket handshake rejected for skew reconnects and re-signs, up to 3 times, with whatever the clock has learned since — so it recovers once a REST response has calibrated the clock, for example when a restored session authenticates the socket before its first REST call returns. An `AUTH_FAILED` frame carries no server time, so a socket-only client whose clock was never calibrated by a REST response still ends in `onReauthenticationRequired`. Responses a browser may have served from cache do not calibrate the clock, and browser clients need an API gateway that exposes `Date` over CORS. See [Error Handling](/sdk/typescript/error-handling).
### Batch-cancel `cancelledAt` is the engine's stamp
`POST /api/v1/orders/batch-cancel`, `POST /api/v1/orders/batch-cancel-all` and `POST /api/v1/orders/batch-cancel-all/{trading_pair_id}` (gRPC `OrdersService.BatchCancelOrders`, `BatchCancelAll`, `BatchCancelAllByPair`) render `cancelledAt` from the matching engine's per-order cancel stamp at millisecond precision, matching, to the millisecond, the `cancelledAt` a later read of the order returns. Previously it came from the API's own clock truncated to whole seconds, which could render a cancel earlier than the order's `createdAt`.
### Funding premium samples are bounded before averaging
Each premium sample is now bounded to the market's maximum funding rate before it enters the window, and every accepted sample in the hour carries equal weight. A brief burst of resting depth can no longer move the settled rate by more than its share of the window. The window, sample cadence and cap are unchanged. See [Window Aggregation](/trading-mechanics/funding-rates#window-aggregation).
### Higher WebSocket limits
Each connection now allows 1,024 subscriptions (was 256) and a message burst of 1,200 (was 300); each account may hold 50 authenticated connections per server instance (was 20). The sustained rate stays 20 messages per second. A per-connection byte budget also refuses a burst of very large frames with `MESSAGE_RATE_LIMIT`, and a channel string longer than 128 bytes, or one that needs JSON escaping, is rejected with `INVALID_SUBSCRIPTION`. See [WebSocket connections](/developers/rate-limits#websocket-connections).
## Upgrade
```bash theme={null}
npm install @0xmonaco/core@1.0.67 @0xmonaco/types@1.0.67 @0xmonaco/react@1.0.67
# or bun
bun add @0xmonaco/core@1.0.67 @0xmonaco/types@1.0.67 @0xmonaco/react@1.0.67
```
Existing calls are source-compatible — method signatures are unchanged. Some response types gain required properties, so a fixture or mock that constructs one must add them: `ScreenerItem` gains `openInterestNotionalAvg1h`, `openInterestNotionalAvg24h` and `openInterestNotionalAvg7d`, and `ScreenerSnapshotPoint` gains `openInterestNotionalAvg` (all `string | null`); `ListConditionalOrdersResponse` and `ListTwapOrdersResponse` gain `nextPageToken: string`. Three behaviours differ: `isolatedMargin` and `liquidationPrice` exclude open-order funds, `listConditionalOrders` / `listTwapOrders` return cursor pages unless you pass `page`, and a recovered liquidation episode sends a terminal `COMPLETED` alert.
# V1.0.69
Source: https://docs.0xmonaco.com/releases/v1.0.69
# Monaco Protocol SDK v1.0.69
This release rolls up the consumer-facing changes shipped since v1.0.67. `sdk.ws.conditionalOrders()` now receives **exactly one versioned update per transition**, TWAP children get **`orders` frames tagged with their `parentOrderId`** and the resting orders they fill get their maker fills, trade fees report the **total fee paid** split into `monacoFee` + `builderFee`, and BuilderCodes payouts name their **payout stream**. Closed positions no longer count a partial-close fee twice, and a position closed over many fills reports its lifetime figures. A risk bucket entering liquidation has its resting orders cancelled at once, a first-use automatically funded `SELL` limit order is no longer refused for want of margin it does not need, the faucet quota is scoped to each wallet and application, and public whitelist submissions are closed. **Action items:** if you consume `conditional_order_update`, handle the new `position_closed`, `reparented` and `expired` reasons and merge by `version`; if you read `fee` from user trades or `orders` fill frames, it now includes the builder fee — read `monacoFee` for Monaco's share; if you call `sdk.whitelist.submit`, it now fails with `403`; and if you use the Rust gRPC SDK, `ListBuildercodePayoutsRequest` no longer derives `Copy`.
## Changed
### One versioned conditional-order update per transition
`sdk.ws.conditionalOrders()` receives exactly one `conditional_order_update` for every conditional-order transition, including OCO sibling cancels (`oco_cancelled`), cancels when the position closes (`position_closed`) or the entry order ends (`parent_cancelled`), activation of a pending leg (`activated`), failed and expired triggers (`expired`), and legs carried onto a replaced entry order (`reparented`). `ConditionalOrderEventReason` gains `position_closed`, `reparented` and `expired`. Every live update carries `version`, the revision of the row it describes — the same value REST and the subscribe-time snapshot return — so merge state from any of the three with `incoming.version >= local.version` when both carry one; a versioned row supersedes an unversioned one. The triggered update carries state `TRIGGERED` with `triggeredOrderId` and `triggeredAt`. Created and cancelled updates are sent once the change is durable, slightly later than before. See [WebSockets](/sdk/typescript/websockets).
### Trade fees report the total fee paid
`sdk.profile.getUserTrades()` (`GET /api/v1/accounts/trades`; gRPC `AccountsService.GetUserTrades`, and `TradesService.ListUserTrades`) returns `UserTrade.fee` as the total fee the user paid on the trade: for a taker, Monaco's taker fee plus the frontend's builder fee (`applicationTakerFee`); for a maker, the maker fee (negative for a rebate). Previously a taker's `fee` left out the builder fee. Two optional components sit beside it, so `fee = monacoFee + builderFee`: `monacoFee` (Monaco's share) and `builderFee` (always `"0"` for a maker, because builder fees are charged to takers only). The accounts `UserTrade` also gains `monacoFeeRaw` and `builderFeeRaw`.
`orders` fill frames (`OrderFilled`, `OrderPartiallyFilled`, `OrderMatched`) carry `fee` as the total fee for the fills they cover and add `monacoFee` / `monacoFeeRaw` and `builderFee` / `builderFeeRaw` (wire `monaco_fee`, `monaco_fee_raw`, `builder_fee`, `builder_fee_raw`). A taker frame's `fee` now includes the builder fee and matches the order echo's `totalTakerFees`; a maker frame's `builderFee` is `"0"`. The components are optional because a server older than them omits them, and `orders` snapshot rows carry no fee fields. No value changes today: every application charges a 0 bps builder fee, so `builderFee` is `"0"` and `fee` equals `monacoFee` on every trade.
### BuilderCodes payouts carry their payout stream
Every `BuildercodePayout` row from `sdk.buildercodes.listPayouts()` (`GET /api/v1/buildercodes/payouts`; gRPC `BuildercodeRewardsService.ListBuildercodePayouts`) gains `kind`: `REVENUE_SHARE` (the frontend's share of Monaco's fee) or `BUILDER_FEE` (the frontend's own builder fee, paid by the taker). Both credit the same claimable balance; every credit today is `REVENUE_SHARE`, because the builder fee a taker pays is not yet credited to the payout bucket. `listPayouts({ kind })` filters to one stream — the SDK rejects any other value before a request is sent, and the server returns `400` (`INVALID_ARGUMENT` on gRPC) for one — and `total` / `totalPages` then count that stream only; with no filter the listing is unchanged. `sdk.buildercodes.getPayoutSummary()` (`GET /api/v1/buildercodes/payouts/summary`; gRPC `GetBuildercodePayoutSummary`) keeps `byToken` as before, summed across both streams, and adds `byTokenAndKind`: one row per token and stream with `token`, `kind`, `totalAmount` (RAW), `payoutCount` and `lastCreditedAt`, and a token's rows add up to its `byToken` total. `@0xmonaco/types` adds `BuildercodePayoutKind` and `BuildercodePayoutTokenKindTotal` with their schemas; `BuildercodePayout.kind` and `GetBuildercodePayoutSummaryResponse.byTokenAndKind` are optional because an older server omits them. In the Rust gRPC SDK, `ListBuildercodePayoutsRequest` no longer derives `Copy` because it now holds a `String` — clone it where code relied on implicit copies. See [BuilderCodes](/sdk/typescript/buildercodes).
### Public whitelist submissions are closed
`sdk.whitelist.submit` (`POST /api/v1/whitelist`; gRPC `WhitelistService.SubmitWhitelist`) rejects every decoded request with `403` `FORBIDDEN` (`PERMISSION_DENIED` on gRPC): `"Public whitelist applications are closed."` The request fields are unchanged for client compatibility, and the legacy success response stays in the generated types but is no longer returned. Existing applicant records and administrative approvals are preserved, and a submission reads or writes no applicant data.
### Faucet quota is per wallet and application
The faucet's daily limit applies to each wallet on each application it signs in to, rather than to the wallet across all applications. `sdk.faucet.mint` (`POST /api/v1/faucet/mint`; gRPC `FaucetService.MintTokens`) enforces it per wallet and application, and `sdk.faucet.getInfo` (`GET /api/v1/faucet/info`; gRPC `FaucetService.GetFaucetInfo`) counts and lists only the calling application's requests. No wire shape changes. See [Faucet](/developers/faucet).
## Added
### `orders` frames for TWAP children and the makers they fill
A child order a TWAP parent places is an ordinary order on the `orders` channel: a taker slice sends `OrderPlaced`, its taker fill and, when the price band stops it short, an `OrderCancelled` for the remainder; a passive child sends `OrderPlaced` when it rests, a maker fill each time it trades, and `OrderCancelled` when it leaves the book (`terminalReason` `REPLACED` on a reprice or deadline sweep, `USER_REQUESTED` when the parent is cancelled or ends). Each resting order a TWAP slice fills receives its maker fill as it would from any other taker. Previously none of these frames were sent. Every TWAP-child frame and `orders` snapshot row carries `parentOrderId`, the same id `getOrder` returns; it is absent for any other order. `CommonOrderEventData` and `OrderSnapshotItem` gain optional `parentOrderId`, and the REST `Order` type now declares it. A resting order that a TWAP slice cancels — for self-trade prevention, or because it failed its risk re-check during the match — receives no `OrderCancelled` frame, so read its terminal state from REST. See [WebSockets](/reference/websockets).
## Fixed
### Resting orders are cancelled when a risk bucket enters liquidation
When a risk bucket breaches maintenance margin and enters `PendingLiquidation`, every one of its resting orders is cancelled, starting in the entry that records the transition — up to 150 there, with any remainder in consecutive entries sequenced immediately after it and nothing interleaved — whether the breach came from an oracle mark, a funding settlement, an auto-deleveraging haircut or a fill — and each sends an `OrderCancelled` frame with `terminalReason` `LIQUIDATION`. Previously those orders stayed on the book until the liquidation itself reached the bucket, and in that window another bucket's liquidation could fill them as makers and deepen the breach. See [Liquidations](/trading-mechanics/liquidations).
### First-use automatically funded sells are sized from their limit
An automatically funded `LIMIT` order on a new risk bucket is sized from its submitted limit price. When a `SELL` executes above that limit, admission tops the bucket up from the parent margin account, bounded by the parent's capacity, instead of leaving it short of initial margin. Previously the sizing read the public best bid, which could include the wallet's own orders that self-trade prevention would cancel, so otherwise affordable orders were refused. This applies to `ISOLATED` and `CROSS` risk buckets, and order-risk previews use the same admission path. Explicit `riskBucketCollateral` stays fee-capped, and unused fee funding is still returned only on `ISOLATED` buckets. See [Trade](/developers/trade).
### Closed positions no longer count a partial-close fee twice
`GET /api/v1/positions` and `GET /api/v1/positions/{position_id}` (gRPC `PositionsService.ListPositions`, `GetPosition`) no longer count a partial-close fee twice. With complete fee history, optional `cumFees` is the position's whole-life fee total — opening and reducing fees, application fees, liquidation fees and signed rebates — and a closed position's `netRealizedPnl` and `realizedRoe` deduct it once. Existing historical attribution errors are not repaired, and a closed position whose fee history is no longer retained keeps its closing-execution `netRealizedPnl` and `realizedRoe`. `feesPaid` remains closing-only and `exitPrice` remains the execution VWAP across all closes. No ledger charges change. See [Positions](/sdk/typescript/positions).
### Positions closed over many fills report lifetime figures
A closed position is served from its reducing executions whenever those executions reconcile against its stored `realizedPnl` to within one raw quote unit (0.000001). The check used to compare them exactly, so ordinary rounding noise pushed healthy positions onto the stored fallback. Affected positions now populate `exitPrice`, `netRealizedPnl`, `realizedRoe`, `fundingPaid` and `feesPaid` where they came back null, and `size` and `isolatedMargin` carry the lifetime closed quantity and allocated margin basis rather than the pre-close remainder. `realizedPnl` is unchanged. Because this is a read-path change, already-closed positions report the corrected values immediately.
**Corrections since this release.** Three statements in the docs as of this release have been superseded; each page is corrected in place:
* *"every account sharing your wallet address, across applications and subaccounts"* (Self-Trade Prevention): the wallet family is every account under your wallet address, across applications and your margin sub-accounts. A master-account sub-account linked under a different wallet address is its own family. See [Self-Trade Prevention](/trading-mechanics/self-trade-prevention).
* *"`strategyKey` ... also echoed on `get_orders` rows at the wire layer"* (Market-Maker Runbook): `strategyKey` is a risk-bucket routing label echoed on the create response and on TWAP orders and events, not on `get_order` rows or the order list. Match client-side by `clientOrderId`. See [Mutation safety](/developers/market-maker-runbook#5-mutation-safety).
* *"Creates a new limit for a sub-account"* (Sub-Account Limits): since v1.0.31, create and update return `400` `Sub-account spending limits are not enforced and can no longer be configured`; get and delete act only on limits created before that change, and all four endpoints are scheduled for removal. See [Sub-Account Limits Endpoints](/api-reference#sub-account-limits-endpoints).
## Upgrade
```bash theme={null}
npm install @0xmonaco/core@1.0.69 @0xmonaco/types@1.0.69 @0xmonaco/react@1.0.69
# or bun
bun add @0xmonaco/core@1.0.69 @0xmonaco/types@1.0.69 @0xmonaco/react@1.0.69
```
Existing TypeScript calls are source-compatible — method signatures are unchanged, and every new response field is optional. The Rust gRPC SDK has one source break: `ListBuildercodePayoutsRequest` no longer derives `Copy`, so code that reuses a moved request must `clone()` it. Four behaviours differ: `ConditionalOrderEventReason` has three more values, so an exhaustive `switch` needs cases for them; `fee` on user trades and `orders` fill frames includes the builder fee; `sdk.whitelist.submit` always fails with `403`; and the faucet quota is counted per application.
# V1.0.7
Source: https://docs.0xmonaco.com/releases/v1.0.7
# Monaco Protocol SDK v1.0.7
This release changes `faucet.mint()` to confirm on-chain before resolving, and fixes balance re-fetch consistency immediately after a live balance update.
## Changed
### `faucet.mint()` now confirms on-chain before resolving
`sdk.faucet.mint()` previously resolved with a partial-success body that you inspected via `minted` and `failed` — even when some or all mints failed. It now resolves **only** when every faucet transaction has confirmed on-chain, and **throws** otherwise:
| Condition | Status | Handling |
| - | - | - |
| All tokens minted and confirmed | `200` | Resolves; `failed` is empty |
| Daily quota exhausted | `429` | Throws; retry after the 24h window resets |
| Some or all mints failed / only partially confirmed | `502` | Throws; the error carries the minted/failed breakdown. Delivered legs still count against the daily quota |
| Faucet temporarily unavailable | `503` | Throws; safe to retry |
Move partial-failure handling out of the response body and into a `try/catch`:
```typescript theme={null}
try {
const { minted, remainingRequests24h } = await sdk.faucet.mint();
// every token in `minted` is confirmed on-chain
console.log(`Minted ${minted?.length ?? 0} tokens, ${remainingRequests24h} requests left`);
} catch (err) {
// 429 quota exhausted, 502 mint failed/partial, 503 unavailable (safe to retry)
console.warn("Faucet mint did not fully succeed", err);
}
```
The `MintTokensResponse` shape is unchanged — on a successful resolve, `failed` is empty.
## Fixed
### Balances re-fetch is consistent after a live update
Re-fetching balances immediately after a `balances` WebSocket event — `sdk.profile.getUserBalances()`, `sdk.profile.getUserBalanceByAssetId()`, or the equivalent `GET /api/v1/accounts/balances` and gRPC `AccountsService.GetBalances` — now reflects that update instead of occasionally returning stale data. No method arguments or response fields changed.
## Upgrade
```bash theme={null}
npm install @0xmonaco/core@1.0.7 @0xmonaco/types@1.0.7 @0xmonaco/react@1.0.7
# or bun
bun add @0xmonaco/core@1.0.7 @0xmonaco/types@1.0.7 @0xmonaco/react@1.0.7
```
`faucet.mint()` now throws on quota-exhausted, failed, or partial mints instead of resolving with a populated `failed` array — update any success-path code that inspected `failed` to catch a thrown error instead. No field names, endpoint paths, or authentication requirements changed.
# V1.0.70
Source: https://docs.0xmonaco.com/releases/v1.0.70
# Monaco Protocol SDK v1.0.70
This release adds **copy trading** — `sdk.copyTrading`, the owner-private `copy` WebSocket channel, React hooks and MCP tools — makes the order and trade listings **cursor-only**, and has order frames name the **margin position an order is bound to** before it fills. Margin order placement that names a `riskBucketId` answers differently for buckets you do not own and for copy trading risk buckets, the movements and funding-payment listings bound their totals by their own pagination reach, and idempotent order submission gets a per-owner receipt share. **Action items:** if you pass `page` to `getPaginatedOrders`, `listConditionalOrders`, `listTwapOrders` or `getUserTrades`, switch to `pageToken` — the SDK no longer accepts it and REST answers `400`; if you read `total` / `totalPages` from user trades, they are gone; and if you place margin orders with an explicit `riskBucketId`, handle the new `404` and `400` answers.
## Breaking
### Order and trade listings are cursor-only
`getPaginatedOrders`, `listConditionalOrders`, `listTwapOrders` and `getUserTrades` (`GET /api/v1/orders`, `GET /api/v1/orders/conditional`, `GET /api/v1/orders/twap`, `GET /api/v1/accounts/trades`; gRPC `OrdersService.ListOrders`, `ListConditionalOrders`, `ListTwapOrders`, `TradesService.ListUserTrades` and `AccountsService.GetUserTrades`) no longer accept `page`. Every call is a cursor walk: send `pageToken` (empty on the first request) and follow `nextPageToken`. `pageSize` has a single ceiling of 1000 on all of them, instead of 100 for page-number requests. REST answers `400` naming `pageToken` when a request still sends `page`, and gRPC drops the field as unknown.
The page-number response fields are removed: `page` from the four listing responses, and `total` / `totalPages` from the user-trades responses, whose table is too large to count. `ListOrdersResponse` keeps `total`, `totalPages` and `totalCapped`; the conditional-order and TWAP listings keep `total`, now populated on the cursor path and bounded at 10,000. A request that sends neither `page` nor `pageToken` used to read only recent rows; it now starts a cursor walk over recent and archived history, so it can return rows it previously could not reach. The `iterateOrders` and `iterateUserTrades` async generators already walk cursors and need no change. `@0xmonaco/types` drops `page` from `GetPaginatedOrdersParams`, `ListConditionalOrdersParams`, `ListTwapOrdersParams` and `GetUserTradesParams` and from their Zod schemas. See [Trades](/sdk/typescript/trades).
### `riskBucketId` answers on margin order placement
Margin orders that name a risk bucket by `riskBucketId` on `POST /api/v1/orders`, `POST /api/v1/orders/batch-create` and `POST /api/v1/orders/twap` (gRPC `OrdersService.CreateOrder`, `BatchCreateOrders`, `CreateTwapOrder`) answer differently:
* A `riskBucketId` that belongs to another user or application returns `404` `Risk bucket not found` (`NOT_FOUND`), exactly as an unknown id does, whatever `tradingPairId` or `marginAccountId` the request carries. Previously it returned `400` `riskBucketId does not belong to tradingPairId`, `400` `riskBucketId does not belong to marginAccountId`, or `404` `Margin account not found`.
* For your own risk bucket the pair and account checks are unchanged: `400` `riskBucketId does not belong to tradingPairId` / `riskBucketId does not belong to marginAccountId` (`INVALID_ARGUMENT`).
* A `riskBucketId` naming one of your risk buckets under a non-default `strategyKey` — your copy trading risk buckets included — returns `400` `riskBucketId names a copy trading risk bucket; copy risk buckets are managed by the engine` (`INVALID_ARGUMENT`). Only the copy engine trades copy trading risk buckets.
Omit `riskBucketId`, name one of your default-strategy risk buckets, or route strategy-keyed orders by `strategyKey` to place a user order. See [Trade](/developers/trade).
## Added
### Copy trading
`sdk.copyTrading` adds thirteen methods over `CopyTradingService` (REST under `/api/v1/copy-trading`). Public, with no session: `listLeadTraders` (sort `roi`, `pnl`, `followers`, `allocated`, `win_rate` or `aum`; window `7d`, `30d`, `90d` or `all`), `getLeadTrader` and `listLeadTraderClosedTrades`. Authenticated: `listLeadTraderOpenPositions` (current followers only), `upsertLeadTrader`, `getMyLeadTrader`, `listMyFollowers`, `upsertFollow`, `previewFollow`, `stopFollow`, `listMyFollows`, `getFollow` and `listFollowLeaderPositions`. `upsertLeadTrader` and an edit through `upsertFollow` replace the settings as sent, so an omitted `bio` or `slPct` is cleared. `previewFollow` (`POST /api/v1/copy-trading/follows/preview`) returns totals only — `estimatedMargin`, `estimatedFee`, `copiedPositions`, `skippedPositions` and `truncated` — and spends the order-risk preview budget; `upsertFollow` accepts `copyExisting` on a create. `getFollow` returns `copyAccountEquity`, the copy account's marked equity.
`sdk.ws.copy(handler, onSnapshot?)` subscribes to the authenticated follower's `copy` channel, carrying `copy_relationship_update` and `copy_skip` frames; `onSnapshot` receives the live follows. React adds `useCopyTrading()` and `useCopyEvents()`, and the MCP server adds the matching copy trading tools. Entry and growth — activating a lead trader, creating a follow, increasing an allocation — are gated by a server-side switch and are refused with `Copy trading is not available` while it is off; stops, pauses and closes are always admitted. See [Copy Trading](/developers/copy-trading).
`MarginAccountSummary` and `PortfolioRiskBucket` gain optional `copyFollowId` and `copyLeaderHandle`, present only on copy risk bucket rows, and order events gain optional `origin` (`USER`, `LIQUIDATION`, `SYSTEM`, `ADL` or `COPY`; absent means not reported) and `copyRelationshipId`. `realizedPnl` on a risk bucket row is the risk bucket's cash balance, not trading profit. See [Margin Accounts](/sdk/typescript/margin-accounts).
### Order frames name their margin position
`orders` events carry optional `positionId` (wire `position_id`), the margin position the order is bound to — the same id `getOrder` returns. A margin order placed while its margin account (cross) or risk bucket (isolated) already holds an open position in the pair is bound when it is admitted, so a resting close or reduce-only order names its position before it fills; otherwise its first fill binds it. It is omitted, never `null`, while the order is unbound, for spot, and by older servers. `OrderPlaced` adds `marginAccountId`, `leverage` and `reduceOnly` for margin orders, and `orders` snapshot rows add `postOnly`, `expirationDate`, `triggerPrice` and `positionSide`, as REST returns them. See [WebSockets](/sdk/typescript/websockets).
## Changed
### Movement and funding-payment totals are bounded by pagination reach
`sdk.profile.getPaginatedUserMovements()` and `sdk.profile.listFundingPayments()` (`GET /api/v1/accounts/movements`, `GET /api/v1/accounts/funding-payments`) return `total` exact up to `pageSize` × 10,000 matching rows and saturated there, and `totalPages` saturated at 10,000, the last page the listing can reach. Wallets under that reach see the same numbers as before; loops that stop at `page >= totalPages` keep terminating. See [Profile](/sdk/typescript/profile).
### Per-owner share for idempotency receipts
Create-order requests that carry an idempotency key share receipt capacity per owner: each user and application may hold at most 10,000 receipts (32 MiB of payload) that are not yet safe to evict — responses still awaiting durable acknowledgement or persistence. An owner at its share gets the existing retryable `ORDER_SUBMISSION_CAPACITY_EXCEEDED` (`503` / `UNAVAILABLE`) for new keys while every other owner keeps placing orders. Retries of an existing key and orders without a key are unaffected, and the share frees up as responses are persisted, expire after 24 hours, or are replaced.
## Fixed
### Delegated replacements check notional in base-asset units
Delegated sessions using `ReplaceOrder` (`PUT /api/v1/orders/{order_id}`) or `BatchReplaceOrders` (`POST /api/v1/orders/batch-replace`) without a `quantity` evaluate `maxOrderNotional` in base-asset units, inheriting the remaining order quantity (or the original quantity when the remaining one is unavailable). A delegated create or replace whose notional is too large to compute is refused with `403` `Delegated agent order notional limit exceeded` (`PERMISSION_DENIED`) instead of failing as a server error. The `POST /api/v1/delegated-agents` types now mark `agentAddress`, `allowedActions` and `allowedTradingPairIds` as required, matching the server. See [Delegated Agents](/developers/delegated-agents).
### Funding history lists only charged windows
`GET /api/v1/market/pairs/{trading_pair_id}/funding/history` and `GET /api/v1/market/funding/history` (gRPC `MarketService.ListFundingHistory`, `ListAllFundingHistory`) list a settlement only once the risk engine has applied it, so every listed window was charged. A window the engine refuses is never charged later, so history can skip that window's time span while epochs stay consecutive. Rows written before this release are unchanged.
## Upgrade
```bash theme={null}
npm install @0xmonaco/core@1.0.70 @0xmonaco/types@1.0.70 @0xmonaco/react@1.0.70
# or bun
bun add @0xmonaco/core@1.0.70 @0xmonaco/types@1.0.70 @0xmonaco/react@1.0.70
```
Code that passes `page` to `getPaginatedOrders`, `listConditionalOrders`, `listTwapOrders` or `getUserTrades`, or reads `page` from their responses or `total` / `totalPages` from user trades, no longer type-checks — switch to `pageToken` / `nextPageToken`. Every other new field is optional.
# V1.0.72
Source: https://docs.0xmonaco.com/releases/v1.0.72
# Monaco Protocol SDK v1.0.72
This release gives copy trading followers **close and top-up controls** — close a follow's copy positions at market while keeping the follow, and add margin to a single copy position from the copy account — makes **fee tiers wallet-level** across applications, ties every **delegated session** to an active grant, and has the `positions` channel send a frame for **every position a fill or a margin move touches**. TWAP updates gain a reconciliation `version`. **Action items:** if you run bots on delegated sessions, an expired or inactive grant now ends reads, refresh and WebSocket access as well as trading, so renew grants before they lapse; and if you keep positions in sync off the `positions` channel, expect more frames per fill and handle the new `margin_added` / `margin_reduced` transfer frames, some of which are unranked.
## Added
### Close copy positions and add copy-position margin
`sdk.copyTrading.closeFollowPositions(followId)` (`POST /api/v1/copy-trading/follows/{followId}/close-positions`; gRPC `CopyTradingService.CloseFollowPositions`) and `sdk.copyTrading.closeAllCopyPositions()` (`POST /api/v1/copy-trading/follows/close-positions`; gRPC `CopyTradingService.CloseAllCopyPositions`) close copy positions at market, reduce-only and immediate-or-cancel, and cancel the resting orders and conditional orders (TP/SL) on the copy risk buckets, **keeping the follows**: the leader's next trades keep mirroring, so a close is not a flatten guarantee. Closes are exits — never gated by copy trading being switched off or an operations block; only a delegated agent session is refused (`403`). Both share a per-account rate limit of about one close per second, burst 5 (`429` with `Retry-After`). The response comes once each batch is durably logged; `closeAllCopyPositions` returns `follows`, `stoppingFollowIds` and `failedFollows` (`{ followId, reason }`, a fixed sentence to retry on).
`sdk.copyTrading.addFollowPositionMargin({ followId, tradingPairId, amount })` (`POST /api/v1/copy-trading/follows/{followId}/position-margin`; gRPC `CopyTradingService.AddFollowPositionMargin`) moves `amount` from the follow's copy account into the margin of its isolated copy position on `tradingPairId`, and returns `followId`, `tradingPairId`, `riskBucketId`, `positionId`, `positionMargin` and `copyAccountAvailable` after the add. The margin account is never touched. It opens no exposure, so it works while copy trading is switched off and on a `CLOSE_ONLY` follow; there is no margin reduction — a follow exits by being stopped. Master account sessions only (`403` for delegated agent and sub-account sessions); `404` for a follow that is not yours; `409` while the follow is stopping or once it is stopped, or with `COPY_TRADING_UNAVAILABLE` when copy trading is not available on the platform; `400` `COPY_TRADING_REJECTED` when the engine refuses; `429` over the movement budget. It is not idempotent: after a timeout, read the follow before retrying.
React's `useCopyTrading()` exposes all three, and the MCP server adds `close_follow_positions` and `close_all_copy_positions` (`market-maker` profile) and `add_follow_position_margin` (fund movement, `full` profile). See [Copy Trading](/developers/copy-trading).
### TWAP updates carry a `version`
`twap_order_update` frames and the `twap_orders` subscribe-time snapshot rows gain an optional `version`. Compare it only within one `twapOrderId`: accept `>=`, ignore lower versions, let the snapshot win an equal-version tie, and expect gaps. An absent version means unknown legacy state, not zero. See [WebSockets](/sdk/typescript/websockets).
## Changed
### Fee tiers are wallet-level
A wallet's rolling 14-day weighted volume (`14d perp + 2.5 × 14d spot`) now sums its trades across every application it trades through, and that one total sets its maker/taker tier on every application. Previously each application tiered the wallet on its own share alone. `GET /api/v1/fees/tier` (gRPC `FeesService.GetMyFeeTier`) keeps its fields; `weightedVolume14d`, `spotVolume14d`, `perpVolume14d`, the tier, `volumeToNextTier` and `nextTierLevel` now report wallet-wide values, the same from any application. The change applies from the next daily recompute at 00:00 UTC, and a wallet signing in to a new application starts there at its wallet tier, not Tier 1. Builder fees (`applicationTakerFee`) are unchanged. See [Fees](/fees).
### Position frames cover every touched position
A fill sends a live `positions` frame for every open position in each risk bucket it touched, taker and makers alike — in a cross risk bucket that is every position of the bucket, in any pair, because a trade in one pair moves the others' liquidation price. Each position is sent at most once per fill, with the fill's `version`.
Collateral transfers send `margin_added` or `margin_reduced` frames. A direct isolated-position margin add or reduction, and a copy-position margin add (the isolated child plus its copy cross risk bucket's positions), are **ranked** at the command's version. Cross-risk-bucket `ALLOCATE` / `RELEASE` and copy follow or stop refreshes are **unranked**, because those commands write no position rows. Wallet `IN` / `OUT` and plain isolated allocation send no position frame. Transfer frames are built from the command's post-command state and released only after its durable acknowledgement. See [WebSockets](/sdk/typescript/websockets#positions-channel).
## Fixed
### Delegated sessions follow their grant
Signed REST and gRPC requests, WebSocket authentication and periodic revalidation, and `POST /api/v1/auth/refresh` all require an active owner/agent grant, including for sessions created before this release. An expired or inactive grant now ends reads and refresh as well as trading. `CreateDelegatedSession` (`POST /api/v1/delegated-agents/sessions`) and refresh return `expiresAt` (Unix seconds) bounded by the grant's expiry; fields are unchanged. Renewing an expired grant requires owner reauthorization, and revocation does not cancel orders. The delegated agent docs also now state that `maxOpenOrders` is unsupported (requests that set it are rejected) and that a grant's market and account scopes limit trading only: an active delegated session can read the owner's account data whatever its scopes. See [Delegated Agents](/developers/delegated-agents).
## Upgrade
```bash theme={null}
npm install @0xmonaco/core@1.0.72 @0xmonaco/types@1.0.72 @0xmonaco/react@1.0.72
# or bun
bun add @0xmonaco/core@1.0.72 @0xmonaco/types@1.0.72 @0xmonaco/react@1.0.72
```
Every new field and method is additive, so existing code type-checks against 1.0.72 unchanged. Two behavior changes still need attention: delegated-session automation loses read, refresh and WebSocket access once its grant expires or goes inactive, so renew grants before they lapse; and a `positions` consumer receives more frames per fill plus `margin_added` / `margin_reduced` transfer frames, some unranked, so match `reason` with a default branch and reconcile unranked frames by field comparison.
# V1.0.73
Source: https://docs.0xmonaco.com/releases/v1.0.73
# Monaco Protocol SDK v1.0.73
This release adds **frontend builder-fee controls and change history**, bounds **resident TP/SL orders**, and hardens withdrawals, margin provisioning, market-price protection and oracle valuation. It also clarifies what an order replacement's `updatedFields` reports, without changing its wire shape. **Action items:** if you maintain many conditional exits, handle the new cardinality refusals and cancel unused legs; if you integrate BuilderCodes, use the payout-wallet session and distinguish the stored fee from the fee currently charged.
## Added
### Set a frontend's builder fee and read its change history
`sdk.buildercodes.setBuilderFee({ bps })` (`PUT /api/v1/buildercodes/builder-fee`; gRPC `BuildercodeRewardsService.SetBuilderFee`) stores an additional taker fee for the authenticated frontend. `bps` is required, an integer from 0 through 100, and a real change must not exceed the live `builderFeeCapBps`. An explicit zero clears the fee. The caller must use a non-delegated frontend application session whose wallet is the application's current payout address, with BuilderCodes enabled; server keys are not accepted.
An enrolled frontend may make two real `BUILDER` changes in a rolling 24 hours; `ADMIN` changes do not count. Setting the stored value again succeeds without a change-log row, email or consumed allowance, even when no changes remain or the cap has since fallen below that stored value. A missing or nonmatching payout address fails the ownership gate first with `403` / `PERMISSION_DENIED`; incomplete enrollment reached after that gate passes and an exhausted allowance return `409` / `FAILED_PRECONDITION`. A change-limit response includes `details.reason: "builder_fee_change_limit"` and `details.nextChangeAllowedAt`. Invalid or above-cap changes return `400` / `INVALID_ARGUMENT`.
`sdk.buildercodes.listBuilderFeeChanges(params)` (`GET /api/v1/buildercodes/builder-fee/changes`; gRPC `BuildercodeRewardsService.ListBuilderFeeChanges`) walks newest-first change history with `pageToken` / `nextPageToken`, default `pageSize` 20 and maximum 100. Rows include `actorType` (`"BUILDER" | "ADMIN"`), `actor`, `oldBps`, `newBps` and `createdAt`; an `ADMIN` actor is the operator's email.
`getConfig()` (`GET /api/v1/buildercodes/config`; gRPC `BuildercodeRewardsService.GetBuildercodesConfig`) adds `builderFeeEnabled`, stored `builderFeeBps`, effective `builderFeeCapBps`, `changesRemaining` and nullable/absent `nextChangeAllowedAt`. A fee can be stored while builder fees are disabled. Charging depends on that separate gate and enrollment, and is capped at the live cap; changes reach new orders within seconds, while resting orders retain their admitted fee. This release does **not** activate `BUILDER_FEE` payout credits. React hooks and MCP tools intentionally omit these frontend configuration operations. See [BuilderCodes](/developers/buildercodes).
## Changed
### Conditional-order cardinality is bounded
Position-attached TP/SL and trailing-stop creates are checked against resident conditional-order caps: defaults are 10 active legs per position, 100 resident legs per user on an engine shard (including pending entry-order legs), and 100,000 resident legs on that shard. Deployments can override these limits. Entry-order TP/SL placement checks the user and shard caps before admission; position attaches also check the position cap. Exceeding a cap refuses the create without creating its new legs. Entry-order placement reports `400` / `INVALID_ARGUMENT` (or a per-item batch rejection), but the released position-attach adapter reports an engine cap refusal as generic `500` `MATCHING_ENGINE_ERROR` / gRPC `INTERNAL`. Not every internal error means a cap was reached; inspect and cancel unused legs before repeating a known over-cap attach.
A full-close TP/SL attach counts the same-type full-close legs it supersedes as retiring, so moving an existing level at the cap does not consume an extra slot. Conditional trigger scanning now rotates through bounded batches rather than repeatedly inspecting the same subset. See [Conditional Perps Orders](/developers/trade#conditional-perps-orders).
### Replacement fields report the resulting order
`ReplaceOrderResponse.updatedFields` and each `batchReplace` result report the replacement's price and **total** quantity, whether those inputs changed or were omitted. Repricing a 60/100-filled order with no quantity reports `quantity: "100"` and rests 40; read `remainingQuantity` for the resting size. Margin reduce-only replacements keep their close-size meaning. This corrects descriptions and JSDoc, with no wire or runtime change. See [Replace Orders](/developers/trade#replace-orders).
## Fixed
### Withdrawals cannot pay back into the vault or regress after execution
`sdk.withdrawals.initiateWithdrawal` (`POST /api/v1/withdrawals`; gRPC `WithdrawalsService.InitiateWithdrawal`) rejects the configured vault address as `destination` with `400` / `INVALID_ARGUMENT`, before debiting funds. A transfer there would leave the tokens in the vault without crediting a recipient. Recording a delayed withdrawal proof also preserves an already-executed withdrawal's terminal status instead of moving it back to confirmed. See [Low-Level Withdrawals](/sdk/typescript/vault#low-level-withdrawals).
### Margin provisioning reports overload as unavailable
When automatic parent margin account creation or cross risk bucket provisioning is refused because the sequencer is full, the API returns retryable `503` / `UNAVAILABLE` instead of `400` / `INVALID_ARGUMENT`. Back off before retrying. Other engine refusals retain their existing classification. See [Error Handling](/sdk/typescript/error-handling).
### Market protection and margin valuation stay consistent
The spot market-order reference counts one price per liquidity-taking order, at its last fill, including after restart; a multi-level sweep cannot contribute several votes to the five-order median. See [Market-order price protection](/trading-mechanics/spot#market-order-price-protection).
Margin collateral transfers now resolve token decimals and symbol from the engine's asset registry, require USDC-family collateral and an exact raw/normalized amount pair, and cannot restate a live account or risk bucket's lifecycle state. Ordinary SDK gateways already send the required collateral shape. Oracle marks that would push a position's notional, margin requirements or unrealized PnL outside the supported valuation range are refused without replacing the last accepted mark; position-margin changes are held to the same bounds, and read-time arithmetic no longer panics on unrepresentable values.
### Oracle delivery supports streams and managed SEDA feeds
Pyth-primary markets can receive prices over WebSocket, with HTTP polling taking over when a stream stops advancing. Managed markets can use a SEDA-served Pyth Pro feed with an explicitly configured Hyperliquid on-close fallback coin, selected when the upstream market is shut rather than on any Pyth failure during an open session; the price source's session informs market policy. Fallback metadata determines the coin's index and rejects missing or delisted coins, but choosing a fallback for the same asset as the Pyth feed remains the operator's responsibility. Availability depends on deployment configuration. Clients continue to use the existing mark/index prices and market-status surfaces. See [Markets](/developers/markets).
## Upgrade
The WebSocket reference's helper, wire-channel and reconnect lists also now include the existing authenticated `copy` subscription. This corrects an omission; it is not a new channel in this release.
```bash theme={null}
npm install @0xmonaco/core@1.0.73 @0xmonaco/types@1.0.73 @0xmonaco/react@1.0.73
# or bun
bun add @0xmonaco/core@1.0.73 @0xmonaco/types@1.0.73 @0xmonaco/react@1.0.73
```
The SDK additions are source-compatible. Handle conditional-order cap refusals, keep withdrawal destinations distinct from the vault, and retry provisioning overloads with backoff. BuilderCodes configuration remains rollout-gated, and stored builder fees do not imply active charging or builder-fee payout credits.
# V1.0.74
Source: https://docs.0xmonaco.com/releases/v1.0.74
# Monaco Protocol SDK v1.0.74
This release adds **margin sub-accounts** with per-account PnL and portfolio views, **secured delegated-agent policies** with IP allowlists and bounded expiry, and **referral tree reads** with the full upline. Withdrawable collateral now counts realized PnL, BuilderCodes payouts start crediting builder fees, and every engine write is now rate limited per account. **Action items:** replace `accountState` with `riskBucketState` on margin-account summaries and liquidation events; set an `expiresAt` (or accept the 14-day default) and use only the supported actions when you upsert delegated agents; handle `429` on position closes, copy-trading settings writes, follow stops and reward transfers; treat `AUTO_DELEVERAGING` as a cancel reason.
## Breaking
### `accountState` is replaced by `riskBucketState`
A margin account has no liquidation lifecycle of its own: every position, order and bad-debt record lives in a risk bucket. `MarginAccountSummary.accountState` (`GET /api/v1/margin/accounts`, `GET /api/v1/margin/accounts/{marginAccountId}`, `GET /api/v1/margin/parent-margin-account`; gRPC `MarginAccountsService`) is removed. The new optional `riskBucketState` (`NORMAL`, `PENDING_LIQUIDATION`, `LIQUIDATING`, `BAD_DEBT`, `CLOSED`) is set only on risk-bucket rows and absent on the parent row. The list `state` filter now matches `riskBucketState`, so any filter excludes the parent row.
`sdk.ws.liquidations` events expose `data.riskBucketState` (wire key `risk_bucket_state`) in place of `data.accountState`. See [Margin Accounts](/sdk/typescript/margin-accounts) and [WebSockets](/sdk/typescript/websockets).
## Added
### Margin sub-accounts
A wallet can hold several margin accounts per application, one per sub-account key. The key `default` is the existing parent margin account: it keeps its id and is the only account deposits, withdrawals and wallet transfers touch. Every sub-account holds its own collateral and its own risk buckets.
* `sdk.marginAccounts.createMarginSubAccount({ subaccountKey, label? })` (`POST /api/v1/margin/sub-accounts`; gRPC `MarginAccountsService.CreateSubAccount`) opens a sub-account. Keys are up to 64 letters, digits, `_`, `-`, `.` or `:`; `copy:` keys are reserved and `default` is refused. The default margin account must exist first. Re-creating a closed key re-opens the same account. Open sub-accounts are capped per owner by weighted 14-day volume tier (10 at the first tier, up to 50) or an admin override; copy-trading sub-accounts do not count.
* `sdk.marginAccounts.transferMarginCollateral({ asset, amount, from, to })` (`POST /api/v1/margin/collateral/transfer`; gRPC `MarginAccountsService.TransferCollateral`) moves collateral between the wallet and the default margin account, or from one margin account to another in one atomic step. `from` and `to` are `{ wallet: true }` or `{ marginAccountId }`. Risk-bucket endpoints are refused on this route; fund a risk bucket with `transferCollateralToRiskBucket`.
* `sdk.marginAccounts.closeMarginSubAccount(marginAccountId)` (`POST /api/v1/margin/sub-accounts/{marginAccountId}/close`; gRPC `MarginAccountsService.CloseSubAccount`) closes a flat sub-account in one call: its flat, solvent risk buckets close with it and all of its collateral moves to the default margin account. A risk bucket in liquidation or bad debt refuses the close, and nothing moves.
* `sdk.marginAccounts.getMarginAccountPnl(marginAccountId)` (`GET /api/v1/margin/accounts/{marginAccountId}/pnl`; gRPC `MarginAccountsService.GetMarginAccountPnl`) returns one margin account's own realized and unrealized PnL; an owner's accounts sum to the owner-wide figure.
* `transferCollateralToRiskBucket` and `simulateRiskBucketOrderRisk` take an optional `marginAccountId` naming any of the caller's open sub-accounts, so a sub-account funds and previews its own risk buckets. A copy-trading sub-account's risk buckets belong to its follow and are refused.
* `sdk.portfolio.getMargin({ marginAccountId, includeAccounts })` (`GET /api/v1/accounts/me/portfolio/margin`; gRPC `AccountsService.GetPortfolioMargin`) describes one margin account, and with `includeAccounts` also returns `accounts` (one row per open margin account) and `ownerTotal`. The response names the account it describes in `marginAccountId`.
* `MarginAccountSummary` gains `subaccountKey` and `marginAccountState` (`ACTIVE` or `CLOSED`), and private order events carry `marginAccountId` (wire `margin_account_id`; a maker fill carries the maker's account), so you can route events per sub-account.
Creating and closing a sub-account and account-to-account transfers spend the per-user movement budget. See [Margin Sub-Accounts](/developers/perps-collateral#margin-sub-accounts) and [Margin Accounts](/sdk/typescript/margin-accounts#sub-accounts).
### Secured delegated-agent policies
`sdk.delegatedAgents.upsertDelegatedAgent` (`POST /api/v1/delegated-agents`; gRPC `DelegatedAgentsService.UpsertDelegatedAgent`) policies now scope with AND: an agent listing a margin account (and markets) trades only that account (on those markets); a market-only agent still trades its markets in every account. `expiresAt` defaults to 14 days from now and may be at most 180 days ahead. `allowedActions` accepts `CREATE_ORDER`, `CANCEL_ORDER`, `REPLACE_ORDER`, `READ` and `MANAGE_RISK_BUCKETS` and rejects anything else; a `READ`-only policy is a read-only key. An owner holds at most 10 active, unexpired agents. The new `ipAllowlist` (up to 20 IP addresses or CIDR ranges) restricts where the agent may create its delegated session (`POST /api/v1/delegated-agents/sessions` returns `403` / `PERMISSION_DENIED` from any other address).
`MANAGE_RISK_BUCKETS` lets an agent move a listed margin account's free collateral into that account's own risk buckets through `transferCollateralToRiskBucket` (`POST /api/v1/margin/risk-buckets/collateral/transfer-in`), only into an isolated risk bucket on a listed market when the policy lists markets. Every other collateral route still refuses delegated agents. See [Delegated Agents](/developers/delegated-agents#policy-design).
### Referral tree and full upline
`sdk.pitpass.getMyReferralTree({ parent?, pageSize?, pageToken? })` (`GET /api/v1/pitpass/referrals/me/tree`; gRPC `TraderCodeService.GetMyReferralTree`) reads one level of your own referral tree a page at a time: the wallets a parent referred, each with its level, parent, referral time, referee count and what you earned from it. Omit `parent` for your L1 referees, or pass one of your L1 or L2 referees to expand it; any other wallet is `404` / `NOT_FOUND`. `pageSize` is 1 to 200, default 50. `getMyReferralPosition` also returns `upline`, your referral chain above you (L1, L2, L3) nearest first. React adds `useMyReferralTree`. See [PitPass](/sdk/typescript/pitpass).
### `AUTO_DELEVERAGING` cancel reason
A cancelled order can report the terminal reason `AUTO_DELEVERAGING`: auto-deleveraging fully closed the position your risk bucket held and this reduce-only order on that pair was cancelled with it. You were deleveraged as a counterparty, not liquidated, so it is not reported as `LIQUIDATION`. Code that switches on `terminalReason` should treat unrecognised values as an opaque cause. See [WebSockets](/sdk/typescript/websockets).
## Changed
### Withdrawable collateral counts realized PnL
`withdrawableCollateral` now counts realized PnL on every surface that publishes it: margin-account summaries, `newWithdrawableCollateral` on collateral transfer responses, the margin figure on `GET /api/v1/margin/collateral/available`, and the portfolio margin totals. On the parent margin account the bound is deposits plus settled realized PnL and funding, less risk bucket allocations, and never more than `freeCollateral` less any unrealized gain; a user who deposited 1,000 and realized 500 can now withdraw 1,500, and a settled loss reduces it. A risk bucket's bound is its allocation plus realized PnL, less funding paid, unrealized losses and required margin; it is no longer capped at the allocation, and it is zero while the risk bucket is not `NORMAL`.
A parent margin account is charged at most a risk bucket's allocation: a loss past it is the insurance fund's. A cross risk bucket that goes flat with no resting orders returns its allocation and realized PnL to the parent margin account and stays open, empty. Collateral a risk bucket draws automatically for an order returns to the parent margin account when that order is cancelled or released, less the share its fills used, and a replace to a smaller order returns only the draw it frees; collateral you moved in yourself stays. Transfers into or out of a risk bucket frozen as bad debt are refused. Field types are unchanged. See [Modify Margin](/developers/positions#modify-margin).
### BuilderCodes payouts credit builder fees
`sdk.buildercodes.listPayouts` and `getPayoutSummary` now return `BUILDER_FEE` credits: each positive builder fee charged on an enrolled application's taker fill accrues in full to the application payout wallet's claimable balance, and `appliedBps` reports the charged builder-fee rate. See [BuilderCodes](/developers/buildercodes).
### Every engine write is rate limited
Every authenticated write that sends a command to the matching engine now draws a per-account limit before it reaches the engine. Eight previously unmetered operations can return `429` `RATE_LIMIT_EXCEEDED` with `Retry-After` and `details.retryAfter` (gRPC `RESOURCE_EXHAUSTED` with `RetryInfo`); a `429` applied nothing.
* **Settings writes** share one budget (1 write/s, burst 10, family 4x): the lead trader profile, follow create/edit and the self-trade-prevention default.
* **Follow stops** have their own budget (1 stop/s, burst 20), so closes and edits never delay a stop.
* **Position closes** draw on the order limit's risk-reduction class: `closePosition` one item, `batchCloseAllPositions` one per open position (the first 100 are charged up front; over budget the whole call is refused).
* **Reward transfers** (`sdk.pitpass.transferRewards`, BuilderCodes claims) spend the per-user movement budget shared with withdrawals and collateral transfers.
Position closes and reward transfers follow each environment's enforcement setting. See [Rate Limits](/developers/rate-limits).
### Position frames rank funding and terminal updates
Position WebSocket updates for non-zero funding settlements and terminal closes, liquidations and ADL are ranked with the durable position `version`. Terminal frames keep the historical position size and exact `CLOSED` or `LIQUIDATED` status; detect closure from status, not a zero size. Zero funding payments emit no position frame. See [Positions](/sdk/typescript/positions).
### Liquidation, deleveraging and bad debt
* **Forced closes ignore self-trade prevention.** Liquidation slices can match the account's own resting orders, and an account's other risk buckets' positions can be ADL counterparties; ADL closes positions directly and fills no resting orders, though a position it fully closes has its resting reduce-only orders on that pair cancelled with `AUTO_DELEVERAGING`.
* **ADL is single-price.** ADL starts when the book refuses the position below three quarters of maintenance margin, fills every chunk at the liquidated position's pinned price, and reaches counterparties in two stages by where the fill leaves their account.
* **The insurance fund's role follows the liquidation mode.** In the default Unbounded mode the terminal slice carries no limit and the insurance fund absorbs fills past the pinned price. When the fund is empty, the venue switches to the Zero mode, where anything the book declines at that price goes straight to ADL. It stays there until it is switched back after the fund is replenished.
* **Bad debt drains from the insurance fund.** A risk bucket in bad debt stays frozen with an immutable allocation; the fund credits it as it pays, possibly in parts, and it unfreezes once covered. There is no manual write-off.
* **A risk bucket's loss stops at its allocation**, for cross and isolated alike, and the engine's live account equity never counts an open loss past it (persisted portfolio and fallback summary reads still sum unrealized PnL uncapped).
See [Liquidations](/trading-mechanics/liquidations) and [Auto-Deleveraging](/trading-mechanics/auto-deleveraging).
### Order margin and margin release
* **A margin limit order's initial margin is priced at its limit**, never the average of its fills; a market order uses its pre-trade estimate.
* **Automatic top-up returns only the unused draw.** An order draws only its shortfall from the parent margin account at admission. Cancelling or releasing it returns the part of that draw its fills did not use, and a replace to a smaller order returns only the draw it frees, for cross and isolated risk buckets alike.
* **Margin release is floored at initial margin.** Reducing a position's margin cannot take it below its initial margin requirement.
See [Margin](/trading-mechanics/perps).
## Fixed
### Position reads report index-price outages as unavailable
`listPositions`, `getPosition` and `getPositionRisk` (`GET /api/v1/positions`, `/positions/{positionId}`, `/positions/{positionId}/risk`) return `503` `SERVICE_UNAVAILABLE` / gRPC `UNAVAILABLE` when a transient index-price read fails, so you can retry; other failures stay `500` / `INTERNAL`. See [Positions](/developers/positions#errors-to-handle).
### Funding publication resumes after epoch gaps
Funding publication resumes when old history occupies newer settlement epochs. Epoch numbers may jump; skipped identifiers are not retroactive charges. The funding endpoints' fields are unchanged. See [Funding Rates](/trading-mechanics/funding-rates).
### Copy-position margin errors
`sdk.copyTrading.addFollowPositionMargin` refuses with `400` `COPY_TRADING_REJECTED` when there is no isolated copy position on the pair, the copy position is being liquidated or is in bad debt, or the amount exceeds what the copy account can put in; `409` covers only a stopping or stopped follow. See [Copy Trading](/sdk/typescript/copy-trading).
## Upgrade
```bash theme={null}
npm install @0xmonaco/core@1.0.74 @0xmonaco/types@1.0.74 @0xmonaco/react@1.0.74
# or bun
bun add @0xmonaco/core@1.0.74 @0xmonaco/types@1.0.74 @0xmonaco/react@1.0.74
```
Rename `accountState` to `riskBucketState` where you read margin-account summaries or liquidation events, and stop expecting a state on the parent row. Review delegated-agent policies for AND scoping and expiry, and retry `429`s after `Retry-After`. Sub-accounts are opt-in; the default margin account and the existing transfer methods are unchanged.
# V1.0.75
Source: https://docs.0xmonaco.com/releases/v1.0.75
# Monaco Protocol SDK v1.0.75
This release moves the remaining page-number listings to **cursor pagination**, makes the **application reporting endpoints public** (selected by `clientId`, no server key), and lists **book orders only** from `GET /api/v1/orders`. It adds **periodic position valuation frames**, **`openedAt` on positions**, **sub-account collateral transfers**, **isolated-only markets**, a **`connecting` WebSocket status** with attempt context, and paged PitPass downline earnings that report every referral level. **Action items:** stop sending `page` and walk listings with `pageToken` / `nextPageToken`; pass `clientId` to the application listings and drop `setServerKey`; list TWAP and conditional orders with their own methods; page `earningsBySource` and read `summary.byLevel`; accept the new `PRICE_BAND` and `SLIPPAGE_TOLERANCE` terminal reasons; upgrade before you rely on valuation frames.
## Breaking
### Page-number pagination is removed
The `page` request field is gone from these listings, all of which now walk with `pageToken`: omit it (or send `""`) to start, then follow `nextPageToken` until it comes back empty.
* Positions: `sdk.positions.listPositions` (`GET /api/v1/positions`; gRPC `PositionsService.ListPositions`) and `listPositionHistory` (`GET /api/v1/positions/history`).
* Margin accounts: `sdk.marginAccounts.listMarginAccounts` (`GET /api/v1/margin/accounts`), `getMarginAccountMovements` (`GET /api/v1/margin/accounts/{marginAccountId}/movements`) and `getParentMarginAccountMovements` (`GET /api/v1/margin/parent-margin-account/movements`).
* Account reads: `sdk.profile.getPaginatedUserMovements` (`GET /api/v1/accounts/movements`), `getUserBalances` (`GET /api/v1/accounts/balances`) and `listFundingPayments` (`GET /api/v1/accounts/funding-payments`).
* Funding history: `sdk.market.listFundingHistory` (`GET /api/v1/market/pairs/{tradingPairId}/funding/history`) and `listAllFundingHistory` (`GET /api/v1/market/funding/history`).
* Application listings: `sdk.applications.listApplicationOrders`, `listApplicationUsers`, `listApplicationMovements` and `listApplicationBalances` (`GET /api/v1/applications/orders`, `/users`, `/movements`, `/balances`).
Over REST a request that still sends `page` is rejected with `400`. Over gRPC protobuf drops the unknown field, so an old client keeps working but reads the **first** cursor page on every call; remove `page` and upgrade your generated stubs. The TypeScript SDK rejects `page` on every listing above with a `ValidationError` before sending, so a caller that handles only `400` sees a client-side error instead.
`pageSize` accepts 1 to 1000 (was 1 to 100). Defaults are unchanged (20, or 50 for funding history). The funding, balances, margin-account and application listings now reject a larger value instead of clamping it. The `page` response field is gone everywhere. Counts change by listing:
* `total` is gone from position history, margin-account movements, and application orders and movements.
* Positions, account movements, funding payments, funding history, application users and application balances keep `total` (and `totalPages` where they had it), bounded by a server count ceiling, with a new `totalCapped` flag that is `true` when more rows matched. Do not drive a paging loop from `total`.
* Balances and margin accounts keep an exact `total` with no `totalCapped`.
Some sort orders change so a walk never repeats or drops a row. Positions sort newest-first by open time (an `OPEN` filter walks by margin account, trading pair and position id), and funding payments by `(createdAt, id)` descending. Funding history sorts by `(settledAt, id)`, application users by user id and application balances by balance id. Balances and margin accounts follow a fixed key order. Rows with no recorded timestamp are no longer listed in margin-account movements, application movements and funding payments.
`@0xmonaco/react`'s `useUserMovements` drops its `page` option and does not take a cursor: it reads one newest-first page and keeps it current from the `movements` channel. Walk the full history with `sdk.profile.getPaginatedUserMovements`. See [Positions](/sdk/typescript/positions), [Profile](/sdk/typescript/profile) and [Margin Accounts](/sdk/typescript/margin-accounts).
### Application reporting endpoints are public
`listApplicationOrders`, `listApplicationUsers`, `listApplicationMovements`, `listApplicationBalances` and `getApplicationStats` (`GET /api/v1/applications/orders`, `/users`, `/movements`, `/balances`, `/stats`; gRPC `ApplicationsService`) no longer use the `x-server-key` header, and application secret keys are no longer issued. Each call takes a required `params` with `clientId`, the public client ID you log in with. A missing `clientId` is `400`, and an unknown or inactive one is `404`. The five share a per-client-IP limit of 60 requests per minute (burst 120); over it REST answers `429` with `Retry-After` and gRPC answers `RESOURCE_EXHAUSTED` with a `google.rpc.RetryInfo` delay. The SDK removes `setServerKey`, and `AppUser` no longer carries `email`, `canWithdraw` or `isBanned`. See [Applications](/sdk/typescript/applications).
### `GET /api/v1/orders` lists book orders only
`sdk.trading.getPaginatedOrders` and `iterateOrders` (`GET /api/v1/orders`; gRPC `OrdersService.ListOrders`) return limit and market orders only, under every `status` and `tradingMode` filter, and `total` counts only those. `Order.orderType` is `"LIMIT" | "MARKET"` again: the `ListedOrderType` alias, its `"TWAP"` / `"CONDITIONAL"` members and the optional `Order.twap` / `Order.conditional` fields are removed. List TWAP parents with `sdk.trading.listTwapOrders` and TP/SL orders with `sdk.trading.listConditionalOrders`; both are unchanged. The request parameters and cursor contract are unchanged. See [Trading](/sdk/typescript/trades).
### PitPass downline earnings are paged
`sdk.pitpass.getMyReferralDownline(params?)` (`GET /api/v1/pitpass/referrals/me/downline`; gRPC `TraderCodeService.GetMyReferralDownline`; React `useMyReferralDownline`) pages `earningsBySource`: a call without paging returns the first 100 rows. Pass `pageSize` (1 to 500, default 100) and `pageToken`, and follow `nextPageToken` until it is empty. Rows are ordered by `totalEarned` descending; a wallet that traded through several applications is merged within a page and can appear again on a later page.
`summary.byLevel` lists `{ level, earned }` for every level with earnings, across every source, on every page. `summary.level1Earned`, `level2Earned` and `level3Earned` are deprecated: they still report levels 1 to 3 and nothing deeper. `getMyReferralPosition`'s `upline` holds as many entries as PitPass currently rewards levels (at most 10), and `getMyReferralTree` expands nodes down to that depth. See [PitPass](/sdk/typescript/pitpass).
### New order terminal reasons
`terminalReason` gains `PRICE_BAND` (the protective price band stopped the order and its remainder was cancelled) and `SLIPPAGE_TOLERANCE` (the order's own `slippageToleranceBps` was the tighter bound). Both used to end `CANCELLED` with no reason. Code that switches exhaustively on `terminalReason` must accept them.
## Added
### Position valuation frames
`sdk.ws.positions(handler, tradingPairId?, onSnapshot?, onValuation?)` takes an optional `onValuation` callback for the channel's periodic `position_valuation` frames (`PositionValuationEvent`). Each frame carries `positionId`, `marginAccountId`, `tradingPairId`, `markPrice`, `unrealizedPnl`, `liquidationPrice` (`null` when it cannot be derived), `initialMarginRequired`, `maintenanceMarginRequired` and `valuedAt`, and no size, side, status or version. Apply a valuation only to a position you hold as `OPEN` whose `updatedAt` is earlier than `valuedAt`; ignore it otherwise. `handler` still receives `position_update` frames only, and without `onValuation` valuation frames are dropped silently. Earlier SDK versions log one parse error per valuation frame, so upgrade before you rely on them. See [WebSockets](/sdk/typescript/websockets).
### `openedAt` on positions
Positions report the instant they opened: the fill that took exposure from zero to non-zero. `Position.openedAt` (`string | null`; `null` when unknown) is on `listPositions`, `getPosition` and the other position reads, and `openedAt` is on `positions` WebSocket events and snapshot rows (absent when the frame does not carry it). It never changes for a `positionId`; a flip opens a new position. Time held is `now - openedAt` while `OPEN` and `updatedAt - openedAt` once terminal. See [Positions](/sdk/typescript/positions).
### Sub-account collateral transfers
`sdk.marginAccounts.transferCollateralToSubAccount(marginAccountId, amount)` (`POST /api/v1/margin/sub-accounts/{marginAccountId}/collateral/transfer-in`; gRPC `MarginAccountsService.TransferCollateralToSubAccount`) moves collateral from the default margin account into a sub-account, and `transferCollateralFromSubAccount` (`.../collateral/transfer-out`; gRPC `TransferCollateralFromSubAccount`) moves it back. Both take a sub-account UUID and a positive decimal amount and resolve to a `TransferMarginCollateralResponse`; the default margin account itself is refused with `400`. The MCP server adds `transfer_collateral_to_sub_account` and `transfer_collateral_from_sub_account`. See [Margin Accounts](/sdk/typescript/margin-accounts#sub-accounts).
### Isolated-only markets
`TradingPair.isolatedOnly` (`GET /api/v1/market/pairs`, `/market/pairs/{tradingPairId}`) marks a market that accepts cross margin only to reduce a position. A `CROSS` order that is not reduce-only is rejected with `400` / `INVALID_ARGUMENT` and the error code `CROSS_MARGIN_NOT_ALLOWED`; open with `marginMode: "ISOLATED"` instead. A copy that would land in a cross risk bucket on such a market is skipped with `MARKET_STATE` and does not count toward the follower skip limit. See [Trading](/developers/trade).
### `connecting` status and connection-attempt context
`onStatusChange` now reports `"connecting"` when a connection opens outside a retry cycle (the constructor's auto-connect, a reconnect after terminal authentication loss, and a `connect()` after a terminal `"disconnected"`), before any other status for that connection. Every status change also passes a `WebSocketStatusInfo`: `attempt`, `hasConnected` and, on `"reconnecting"`, `nextRetryDelayMs`. `"reconnecting"` with `hasConnected: false` means the first connect is still failing. With a finite `maxReconnectAttempts`, running out reports one `"disconnected"` whose `attempt` equals the cap, and a later `connect()` starts a fresh retry budget. Behavior changes: `getStatus()` returns `"reconnecting"` while a retry is opening, and an error thrown or rejected by `onStatusChange` is logged instead of propagated. See [WebSockets](/sdk/typescript/websockets).
## Changed
### Fee tiers count volume floors
`sdk.fees.getMyFeeTier` (`GET /api/v1/fees/tier`; gRPC `FeesService.GetMyFeeTier`) resolves `currentTierLevel` and `volumeToNextTier` from the higher of the wallet's weighted 14-day volume and any volume floor Monaco has set on it. `weightedVolume14d`, `spotVolume14d` and `perpVolume14d` still report recorded volume, so a wallet with a floor can show a tier above what `weightedVolume14d` alone reaches. No fields changed. See [Trading](/sdk/typescript/trades).
### Margin order admission prices the real cost
A crossing order is charged its initial margin at the mark, the loss each fill takes against the mark, and its taker fee. A resting order is charged its initial margin at its limit, its maker fee, and the loss it would take against the live mark. Only the part of an order that opens or flips a position is charged, so a close is never refused for its loss. The parent margin account's automatic draw funds exactly what a risk bucket's orders need beyond its own equity, and every release returns the rest. An adjust-margin transfer prices the amount at the current mark, so the position's initial margin requirement rises by exactly the amount. No request or response shape changed. See [Margin](/trading-mechanics/perps).
### Collateral bounds round down
`withdrawableCollateral` and `availableOrderCollateral` on margin-account and risk-bucket rows, and `newWithdrawableCollateral` on transfer responses, round down to the collateral asset's decimals. A transfer of exactly the printed `withdrawableCollateral` is accepted; `availableOrderCollateral` is the order-admission bound, not a transfer bound. Risk-bucket figures (`isolatedMargin`, equity, `freeCollateral` and the cross `liquidationPrice`) no longer count the parent's automatic draw for a resting order against the position.
## Fixed
* **Account-to-account transfers** — `transferMarginCollateral` between two of your own margin accounts no longer fails with `400` "token is not a valid 20-byte address"; the `asset` field is accepted and ignored on that route.
* **TP/SL on a just-opened position** — `sdk.positions.attachPositionTpSl` (`POST /api/v1/positions/{positionId}/tp-sl`) no longer answers `404` while the new position is still being persisted.
* **Adding margin right after open** — `transferCollateralToRiskBucket` (Adjust Margin) and `sdk.copyTrading.addFollowPositionMargin` credit a position that has just opened instead of treating the add as a plain risk-bucket allocation.
* **Margin TWAP on a new risk bucket** — `sdk.trading.createTwapOrder` on an isolated risk bucket that does not exist yet, or is closed, moves no collateral up front; each slice draws what it needs as it reserves.
## Upgrade
```bash theme={null}
npm install @0xmonaco/core@1.0.75 @0xmonaco/types@1.0.75 @0xmonaco/react@1.0.75
# or bun
bun add @0xmonaco/core@1.0.75 @0xmonaco/types@1.0.75 @0xmonaco/react@1.0.75
```
Remove every `page` argument and loop on `nextPageToken`. Replace `setServerKey` with a `clientId` on each application listing call. Move TWAP and TP/SL reads off `getPaginatedOrders`, read `summary.byLevel` for referral earnings, and add `PRICE_BAND` and `SLIPPAGE_TOLERANCE` to any exhaustive `terminalReason` handling.
# V1.0.76
Source: https://docs.0xmonaco.com/releases/v1.0.76
# Monaco Protocol SDK v1.0.76
This release finishes the move to **cursor pagination** on the public trades listing and the pending-withdrawal, managed-market and BuilderCodes payout listings, and makes `sdk.trades.getTrades` resolve to a `{ trades, nextPageToken }` envelope. Perp risk tiers report **`maxPositionNotional` as a USD notional cap**, and markets can carry an optional USD position cap and open-interest cap checked at admission. BuilderCodes reads and claims no longer depend on the revenue-share switch. The WebSocket `market_stats` and `instruments` channels carry more market context, and an edited order's `OrderPlaced` reports the fill it carried over. **Action items:** stop sending `page` to these four listings and walk them with `pageToken` / `nextPageToken`; read `.trades` from `getTrades`; read `maxPositionNotional` instead of `maxPositionSize`; handle a successful BuilderCodes config read with `buildercodesEnabled: false`.
## Breaking
### Page-number pagination is removed from four more listings
The `page` request field is gone from these listings, which now walk with `pageToken`: omit it (or send `""`) to start, then follow `nextPageToken` until it comes back empty. Each walk is one ordered read, so an empty `nextPageToken` is a definitive end.
* Public trades: `sdk.trades.getTrades` (`GET /api/v1/trades/{tradingPairId}`; gRPC `TradesService.GetTrades`). `pageSize` defaults to 25. The walk seeks on `(executedAt, id)`, so a page never repeats or skips a trade when new fills land mid-walk, and still covers the recent trades window rather than archived history.
* Pending withdrawals: `sdk.withdrawals.listPendingWithdrawals` (`GET /api/v1/withdrawals`; gRPC `WithdrawalsService.ListPendingWithdrawals`). Default 20, newest first.
* Managed-market assignments: `sdk.managedMarkets.listAssignments` (`GET /api/v1/managed-markets/assignments`; gRPC `ManagedMarketsService.ListAssignments`). Default 100, **oldest** first.
* BuilderCodes payouts: `sdk.buildercodes.listPayouts` (`GET /api/v1/buildercodes/payouts`; gRPC `BuildercodeRewardsService.ListBuildercodePayouts`). Default 20.
`pageSize` accepts 1 to 1000 (was 1 to 100), and a larger value is rejected instead of clamped. The `page` response field is gone; `total` (and `totalPages` on pending withdrawals and payouts) stays exact. Over REST a request that still sends `page` is rejected with `400`. Over gRPC an old stub's `page` is discarded as an unknown field and every call reads the **first** page, so regenerate your stubs. The TypeScript SDK and the MCP server reject a leftover `page` before sending.
**Payout ordering changes.** `listPayouts` now walks `(createdAt, id)` descending. It never skips or repeats a credit, but it is not chronological inside one persistor batch: every credit a batch writes shares a timestamp and the `id` tie-break is random, so a later fill can be listed ahead of an earlier one. Across batches the order is still newest-first. See [BuilderCodes](/sdk/typescript/buildercodes).
### `getTrades` returns an envelope
`sdk.trades.getTrades(tradingPairId, options)` resolves to `{ trades, nextPageToken }` instead of `TradeEvent[]`. `options.page` is gone and `options.pageToken` is new; passing `page` throws a `ValidationError` before any request is sent. Callers that used the array directly read `.trades`. The trades and their order (newest first) are unchanged. The MCP `get_trades` tool takes `pageToken` and returns `nextPageToken`, and its strict input schema rejects `page`. `useTradeFeed` is unchanged. See [Market Data](/sdk/typescript/market).
### `maxPositionNotional` is a USD notional cap
On `getPerpMarketConfig` (`GET /api/v1/market/pairs/{tradingPairId}/perp/config`), each `RiskTier.maxPositionNotional` is now the most USD notional a position may hold at that tier's `maxLeverage`: the next tier's `lowerBoundNotional`, or the market's position cap on the top tier (absent when the market is uncapped). It used to carry a base-asset quantity under the notional name. `maxPositionSize` is deprecated and no longer set. See [Margin ladders](/trading-mechanics/perps#margin-ladders).
## Added
### Market position and open-interest caps
A perp market can carry an optional USD **position cap** and an optional USD **open-interest cap**. Both are checked at admission only, priced at the mark. An order that would take its risk bucket's position plus its same-side resting orders past the position cap is rejected with `position cap exceeded: …`, and one whose opening quantity would take the market's open interest past its cap with `open interest cap reached: …`, both `400` / `INVALID_ARGUMENT`. A position the mark carries past a cap can still be reduced, and a reduce-only order always passes. The per-base-quantity position size limit is retired, so a market is uncapped unless configured otherwise. See [Margin ladders](/trading-mechanics/perps#margin-ladders).
### More fields on `market_stats` frames
`sdk.ws.marketStats` events and snapshot rows carry `openInterestHeadroom` (what is left of the open-interest cap at the mark, in USD notional, never below zero) and `marketStatus` (the same label as the market's `marketStatsAll` row; pass unrecognized values through). Perp frames add `fundingIntervalSeconds` and `lastFundingTime` (a millisecond epoch, absent before the first settlement). `openInterestLimit` is now the cap in USD notional; it and `openInterestHeadroom` are omitted when the market is uncapped. A perp market is always published: until its open interest is known, `openInterest` and `openInterestHeadroom` are omitted (never `"0"`) and every other field is still sent. See [WebSockets](/sdk/typescript/websockets).
### Asset identity on `instruments` frames
`sdk.ws.instruments` snapshot rows and live frames carry `baseAssetId`, `quoteAssetId`, `baseToken`, `quoteToken`, `baseAssetName`, `quoteAssetName`, `baseDecimals` and `quoteDecimals` on every market, plus `minLeverage` on perps, so a client can label a market and scale its amounts without parsing `symbol` or calling REST. All are optional; a frame from an older server arrives with them `undefined`.
### Edited orders report their carried fill
Replacing a partly filled order keeps its fills. The replacement's `OrderPlaced` now carries `filledQuantity`, `filledQuantityRaw` and `averageFillPrice` from the original, with the order's total `quantity` and `PARTIALLY_FILLED`, as `getOrder` reports it. Its fill frames report the total, cumulative fill and average across every fill. A maker fill's `averageFillPrice` is now the order's cumulative average rather than that trade's price, which stays in `executionPrice`. See [WebSockets](/sdk/typescript/websockets).
### Per-market perp price band
A perp market can be configured with its own price band, from 100 to 2,500 bps, which replaces both the 1,000 bps placement default and the 1,200 bps triggered-leg default on that market. Markets without one keep the defaults. See [Perp price band](/trading-mechanics/perps#perp-price-band).
## Changed
### BuilderCodes reads and claims are not switch-gated
`getConfig`, `listPayouts`, `getPayoutSummary`, `getRewardsBalance`, `claimBuildercodeRewards` and `listBuilderFeeChanges` (`GET /api/v1/buildercodes/config`, `/payouts`, `/payouts/summary`, `/rewards/balance`, `/builder-fee/changes`, `POST /api/v1/buildercodes/rewards/claim`) work whether or not the revenue share is on. They return `403` / `PERMISSION_DENIED` only for a delegated session or, where it applies, a caller that is not the application's payout wallet. `setBuilderFee` (`PUT /api/v1/buildercodes/builder-fee`) is refused only while both the revenue share and builder fees are off. `getConfig`'s `buildercodesEnabled` reports whether the revenue share is on and can be `false` on success. See [BuilderCodes](/developers/buildercodes).
### Overload refusals on cancels
When the engine's work budget is full, cancels, like creates and replaces, can be refused with a retryable `503` and error code `OVERLOADED`. Cancels keep a reserved share of that budget, so they are refused last.
### Direct vault deposits need the exact payload
A deposit made straight to the vault contract must carry the exact `applicationData` that `encodeDepositApplicationData()` (from `@0xmonaco/contracts`) produces. Apart from Monaco's own insurance-fund top-up form, any other JSON-shaped payload is credited to the spot wallet of Monaco's default application rather than yours. SDK deposits are unaffected. See [Deposit Tokens](/deposit/deposit-tokens).
## Fixed
* **`useUserOrders` and edited orders** — the replacement of a partly filled order now shows the fill and average it carried over, instead of zero fill until a refresh.
## Upgrade
```bash theme={null}
npm install @0xmonaco/core@1.0.76 @0xmonaco/types@1.0.76 @0xmonaco/react@1.0.76
# or bun
bun add @0xmonaco/core@1.0.76 @0xmonaco/types@1.0.76 @0xmonaco/react@1.0.76
```
Remove `page` from `getTrades`, `listPendingWithdrawals`, `listAssignments` and `listPayouts` and loop on `nextPageToken`. Read `.trades` from `getTrades`. Read `maxPositionNotional` as a USD notional and drop `maxPositionSize`. Treat `buildercodesEnabled: false` as a valid config response.
# V1.0.77
Source: https://docs.0xmonaco.com/releases/v1.0.77
# Monaco Protocol SDK v1.0.77
This release makes an edit to a **margin reduce-only order** read `quantity` as the order's new **total**, like every other order. The all-markets `market_stats` rows carry perp index, funding and open-interest notional values, the `instruments` snapshot includes inactive markets, and TWAP taker slices now send the same WebSocket frames as any other taker fill. **Action items:** send the order's total, not `total - filled`, when editing a reduce-only order; filter `instruments` rows on `isActive` when you need the tradable set.
## Breaking
### Reduce-only edits take the order's total
`sdk.trading.replaceOrder` (`PUT /api/v1/orders/{orderId}`; gRPC `OrdersService.ReplaceOrder`) and each item of `sdk.trading.batchReplace` (`POST /api/v1/orders/batch-replace`; gRPC `OrdersService.BatchReplaceOrders`) now read a margin reduce-only order's `quantity` as its new total. The replacement rests `total - filled`: a close of 0.8 with 0.3 filled, edited to `quantity: "0.5"`, rests 0.2 and reads 0.3/0.5. Before, it rested 0.5 and read 0.3/0.8.
* A total at or below the filled amount is refused with `New quantity N must be greater than filled quantity F`, where it used to be accepted as a smaller close.
* What the replacement rests is still bounded by the live position, so a total whose remainder exceeds the position is refused.
* An exact full close of the remaining position is still admitted below the minimum order size.
* With `quantity` omitted, the order keeps its total and rests the live remainder, as before. `updatedFields.quantity` (gRPC `updated_quantity`) now reports that total, not the remaining close size.
No request or response fields changed. Callers that sent `total - filled` as a workaround must now send the total. See [Replace Orders](/developers/trade#replace-orders).
## Added
### Index, funding and open-interest notional on `marketStatsAll` rows
Each perp row of `sdk.ws.marketStatsAll` (the all-markets `market_stats` frame and its subscribe-time snapshot) carries `indexPrice`, `fundingRate`, `premium` and `openInterestNotional`. `fundingRate` is the estimated next-window rate; `currentFundingRate` stays the last settled one. `openInterestNotional` is open interest priced at the mark, falling back to the last trade price, in quote units. It is omitted (never `"0"`) until both open interest and a price are known, while REST `getPerpMarketSummary` reports `"0"` in that case. All four are omitted on spot rows and on rows from an older server. See [WebSockets](/sdk/typescript/websockets).
## Changed
### The `instruments` snapshot includes inactive markets
`sdk.ws.instruments` baselines, both the server snapshot and the replay a late subscriber gets, now include inactive (delisted) markets flagged `isActive: false`, so positions, orders and history on a delisted market can still resolve its metadata. `useInstruments` keeps those rows too. Filter on `isActive` to build the tradable set. See [useInstruments](/sdk/react/hooks/use-instruments).
## Fixed
* **TWAP taker slices send every taker frame** — a slice that takes now sends the public trade (`sdk.ws.trades`), order book and live candle updates, both sides' `balances` updates, and `positions` updates for both sides, including the terminal `CLOSED` update when it closes a position. Before, it sent only its own `orders` frames. A passive TWAP child that rests or leaves the book now updates the order book, and a spot child that leaves it sends its owner the `movements` frame for the released lock, as a manual cancel does.
## Upgrade
```bash theme={null}
npm install @0xmonaco/core@1.0.77 @0xmonaco/types@1.0.77 @0xmonaco/react@1.0.77
# or bun
bun add @0xmonaco/core@1.0.77 @0xmonaco/types@1.0.77 @0xmonaco/react@1.0.77
```
Send the order's total when editing a reduce-only order. Filter `instruments` rows on `isActive` for the tradable set.
# V1.0.80
Source: https://docs.0xmonaco.com/releases/v1.0.80
# Monaco Protocol SDK v1.0.80
This release rolls up the consumer-facing changes shipped since v1.0.77. Positions report whether their lifetime totals are complete, closed and liquidated position lists sort newest-closed first, and a flat margin account now gets an `account` frame when its collateral moves. Perp `instruments` frames carry the whole margin ladder, the `trades` channel sends the newest 50 trades on subscribe, risk-bucket rows report `maxRemovableCollateral`, and the Rust gRPC SDK gains an optional `serde` feature. **Action items:** restart a `status=CLOSED` or `status=LIQUIDATED` position walk with an empty `pageToken`; read `lifetimeTotalsStatus` before trusting a position's lifetime totals, and never read an absent total as zero; handle `timeInForce: null` on MARKET order events; parse insufficient-balance amounts as asset units, not raw integers.
## Added
### `lifetimeTotalsStatus` on positions
`sdk.positions.listPositions` (`GET /api/v1/positions`; gRPC `PositionsService.ListPositions`) and `sdk.positions.getPosition` (`GET /api/v1/positions/{positionId}`; gRPC `PositionsService.GetPosition`) return `lifetimeTotalsStatus` on every position. It says whether the lifetime fields (`exitPrice`, `realizedRoe`, `feesPaid`, `fundingPaid`, `cumFees`, `netRealizedPnl`) can be trusted:
* `LIFETIME_TOTALS_STATUS_COMPLETE` — the position history covers the whole lifetime, and the lifetime fields follow their existing rules.
* `LIFETIME_TOTALS_STATUS_PENDING` — history is still being written, or the row changed while it was being read. Every lifetime field is absent. Retry the read after a short backoff (seconds).
* `LIFETIME_TOTALS_STATUS_INCOMPLETE` — the totals are not complete now, and a quick retry will not change that. A gap in legacy history is permanent; a missing or drifted totals row is repaired by a background reconcile, after which the position reads `COMPLETE`.
A lifetime field that is present is cross-checked against the others, so `exitPrice` can be present while `cumFees` is absent. Never read an absent total as zero. `OPEN` and `LIQUIDATING` positions are now reconciled against their `realizedPnl` like closed ones: one whose history does not account for it reports `INCOMPLETE` and no longer returns `cumFees`, which it used to return understated. Closed, liquidated and expired positions return the same lifetime fields as before. WebSocket position frames do not carry the field. `@0xmonaco/types` adds the `LifetimeTotalsStatus` type, `usePositions` exposes the field, the MCP `get_position` / `get_positions` tools return it, and the Rust REST and gRPC SDKs gain `lifetime_totals_status`. See [Positions](/developers/positions).
### The margin ladder on `instruments` frames
`sdk.ws.instruments` frames for a perp market carry `riskTiers`, the whole margin ladder ascending by `tierLevel`, with the same fields as the REST perp config's `riskTiers`, on both the subscribe-time snapshot and live frames. Any margin-ladder edit now sends a `config_change` frame whose `changedFields` names `risk_tiers` (plus `max_leverage`, `initial_margin_rate` and `maintenance_margin_rate` when tier 1 changed). Spot frames omit the field, and a frame from an older server arrives with `riskTiers` `undefined`. See [WebSockets](/sdk/typescript/websockets).
### A subscribe-time snapshot on `trades`
`sdk.ws.trades(tradingPairId, handler, onSnapshot?)` takes an optional `onSnapshot` that receives the market's newest 50 trades, newest first, when you subscribe. It is a recent-history baseline, not a gap-free replay: it can overlap the first live trades or miss one executed just before you subscribed, so dedupe on `tradeId`. See [WebSockets](/sdk/typescript/websockets).
### `maxRemovableCollateral` on risk-bucket rows
Risk-bucket rows of `sdk.marginAccounts.getMarginAccountSummary` and `getParentMarginAccountSummary` (`MarginAccountSummary`, `GetMarginAccountSummaryResponse`) and of `sdk.portfolio.getMargin` (`PortfolioRiskBucket`) carry `maxRemovableCollateral`: the largest removal the risk engine accepts when it may also lower the posted margin of an isolated bucket's one open position, down to its initial margin at the mark. The part past `withdrawableCollateral` comes out of the position's margin, raising its leverage and moving its liquidation price toward the mark. It equals `withdrawableCollateral` on a cross bucket and on an isolated one with no open position or several, and is present only on live-engine reads. `withdrawableCollateral` is unchanged. See [Margin Accounts](/sdk/typescript/margin-accounts).
### Optional `serde` feature in the Rust gRPC SDK
`monaco-grpc-sdk` gains an optional `serde` feature. With it on, every generated message and enum derives `Serialize` / `Deserialize` in the JSON shape the REST API uses: camelCase field names, the uint64 lease and quote fences as decimal strings, unset optional fields omitted where REST omits them, and the REST snake\_case aliases accepted. It is off by default and changes nothing on the gRPC wire. See [Rust gRPC SDK](/sdk/rust-grpc).
## Changed
### Closed and liquidated positions list newest-closed first
`sdk.positions.listPositions` (`GET /api/v1/positions`; gRPC `PositionsService.ListPositions`) with `status=CLOSED` or `status=LIQUIDATED` now orders by `(closedAt, positionId)` descending, so a long-held position closed today is on the first page. Every other filter keeps its order. A `status=CLOSED` or `status=LIQUIDATED` `pageToken` issued before this release is rejected with `400`; restart that walk with an empty `pageToken`. A position closed from this release on takes the instant the matching engine emitted the close as its close time, and a terminal position's `updatedAt` reports it. No request or response fields changed.
### Flat margin accounts get `account` frames
`sdk.ws.account` now sends a live `account_update` frame for a flat margin account (no open position and no resting order) after its collateral moves: a transfer between the spot wallet and margin, an on-chain deposit into margin or a withdrawal from it, a transfer between your margin accounts, closing a sub-account, a copy-trading allocation, or an insurance payout. Moves that land close together collapse into one frame carrying the latest state, normally within about 2 seconds. The frame uses the account's existing `sequence`, so the dedupe rule is unchanged. A flat account still sends no periodic keepalive. No wire-shape change.
## Fixed
* **Market orders report no time in force** — every WebSocket order event of a MARKET order carries `timeInForce: null`, and `Order.timeInForce` is absent (`null` on the orders snapshot), including for existing market orders. Before, `OrderPlaced` said `GTC`, and engine-placed market orders such as TWAP slices and copy orders said `IOC`. LIMIT orders keep their time in force.
* **Win rate counts fees** — the `winners`, `losers` and `winRatePct` of `sdk.portfolio.getRealizedPnl` and the copy-trading leader `winRatePct` classify a perp close by its realized PnL net of fees. Before, a close that was profitable before fees but lost money after them counted as a winner. Closes counted before this release are not reclassified.
* **Cross transfers into a closed risk bucket move the money** — a transfer into a cross risk bucket that had been emptied and closed now reopens it and moves the collateral. Before, it could report success without moving anything.
* **Isolated order previews work after the bucket closes** — `sdk.marginAccounts.simulateRiskBucketOrderRisk` on a pair whose isolated risk bucket was closed now previews like a first isolated order, instead of returning a permanent "still being provisioned" `400`.
* **Insufficient-balance errors show asset units** — a wallet shortfall on a spot order, a margin collateral transfer-in or a withdrawal reports `Available` and `Required` in the asset's units (`Required: 5000`, not `Required: 5000000000`). A transfer or withdrawal above the account's whole balance now returns the insufficient-balance error instead of an isolated-collateral message.
* **A risk bucket leaves bad debt flat** — once the insurance fund covers a bucket's shortfall, an isolated risk bucket closes and a cross risk bucket returns to normal, empty. See [Liquidations](/trading-mechanics/liquidations).
* **Referral and BuilderCodes reads are per client application** — the docs now say that `getMyReferralPosition().upline` lists the levels your trades on this client application pay, and can be empty while `referrer` is set; `getMyReferralTree()` goes as deep as any client application rewards (at most 10); and the BuilderCodes config fields describe this application's values. No wire change.
## Upgrade
```bash theme={null}
npm install @0xmonaco/core@1.0.80 @0xmonaco/types@1.0.80 @0xmonaco/react@1.0.80
# or bun
bun add @0xmonaco/core@1.0.80 @0xmonaco/types@1.0.80 @0xmonaco/react@1.0.80
```
Restart closed and liquidated position walks with an empty `pageToken`. Check `lifetimeTotalsStatus` before using a position's lifetime totals.
# MCP Server
Source: https://docs.0xmonaco.com/sdk/mcp-server
Connect Monaco to Claude, Cursor, Codex, Windsurf, and any MCP-compatible assistant.
Monaco is built to be driven by AI. Paste the config below into your assistant to expose Monaco's market data and trading as callable [MCP](https://modelcontextprotocol.io) tools — no code required.
Two modes are available:
* **Public mode** — no credentials; read-only market data, orderbook, and trades are available immediately.
* **Authenticated mode** — add your wallet private key, client ID, and network to enable authenticated tools. Which tools are enabled is bounded by a [capability profile](#capability-profiles): supplying a key does **not** blindly unlock everything. The default profile is read-only; trading requires opting in to `market-maker`, and fund movement (deposits, withdrawals, reward transfers) requires `full`.
The MCP server is a **command and snapshot plane**: it places, replaces, and cancels orders and takes one-shot reads (`get_orders`, `get_positions`, `get_balances`, `get_orderbook`). It has **no streaming or subscription tools** — every read is a point-in-time snapshot. For live fills, cancels, book movement, and position/risk changes, pair it with the Core SDK [WebSocket state plane](/sdk/typescript/websockets). If you are running a market maker or any continuously-managed strategy, follow the [Market-Maker Runbook](/developers/market-maker-runbook).
## Add the MCP server
```json Claude/Cursor theme={null}
{
"mcpServers": {
"monaco": {
"command": "npx",
"args": ["-y", "@0xmonaco/mcp-server"],
"env": {
"MONACO_NETWORK": "staging"
}
}
}
}
```
```bash Claude Code theme={null}
claude mcp add monaco \
-e MONACO_NETWORK=staging \
-- npx -y @0xmonaco/mcp-server
```
```toml Codex theme={null}
# ~/.codex/config.toml
[mcp_servers.monaco]
command = "npx"
args = ["-y", "@0xmonaco/mcp-server"]
env = { MONACO_NETWORK = "staging" }
```
That's the **public, read-only** server — live market data, orderbook, and trades with zero credentials. Restart your assistant after editing a config file.
To enable authenticated tools, add your wallet private key, client ID, and network. By default this grants read-only access (balances, orders, fee tier); set `MONACO_CAPABILITY=market-maker` to place and cancel orders, and `MONACO_CAPABILITY=full` for vault fund movement. Full per-client setup, authenticated config, capability profiles, and the complete tool list are below.
## Point your AI at the docs
These docs are LLM-readable, so any model or agent can ingest them directly:
| File | What it is |
| - | - |
| [`/llms.txt`](https://docs.0xmonaco.com/llms.txt) | A structured index of every page — point an agent here to let it crawl on demand. |
| [`/llms-full.txt`](https://docs.0xmonaco.com/llms-full.txt) | The entire documentation as one plain-text file — paste it straight into a model's context. |
Every page also has **Copy page** and **Open in ChatGPT / Claude** buttons in the top-right, and appending `.md` to any docs URL returns the raw Markdown.
## Drive Monaco in natural language
Once the MCP server is connected, just ask:
> What's the current ETH/USDC orderbook on Monaco, and how does the mid price compare to the last few trades?
In authenticated mode the agent can place and cancel real orders. Start on `staging` (testnet) and review what it proposes before confirming trades.
## Trading with MCP
Authenticated mode lets an MCP-compatible assistant act through the wallet configured by `MONACO_PRIVATE_KEY`, within the bounds of the active [capability profile](#capability-profiles). The default `read` profile lets the agent inspect balances, open orders, and fees only. Set `MONACO_CAPABILITY=market-maker` to let it request testnet funds, place spot or perp orders, replace orders, and cancel orders. Moving funds through the Monaco vault tools (deposit, withdraw) and transferring rewards require the `full` profile — they are off unless explicitly enabled.
Use this mode when you want an assistant to operate Monaco directly from natural language, for example:
* “Check my balances, then place a small ETH/USDC limit buy 5% below mid.”
* “Cancel my stale open orders before placing a new quote.”
* “Simulate fees for this order before submitting it.”
MCP is not a standalone market-making surface. Because it does not stream, an agent that manages quotes off intermittent MCP snapshots alone will miss fills, cancels, and book movement between calls — leaving stale quotes and one-sided exposure. Drive commands through MCP, but track live state through the Core SDK [WebSocket](/sdk/typescript/websockets). See the [Market-Maker Runbook](/developers/market-maker-runbook) for the full command-plane vs state-plane loop.
Authenticated MCP tools control the configured wallet on the selected network. Use `staging` first, keep private keys out of committed config files, and review trading actions before approving them.
## Capability profiles
Authenticated mode is scoped by `MONACO_CAPABILITY`. The profile bounds which tools the server registers — a tool outside the active profile (and not named in `MONACO_TOOL_ALLOWLIST`) is **never registered**. This is an authorization control, not an advisory annotation.
| Profile | Tools enabled |
| - | - |
| `read` | Authenticated read-only queries — profile, balances, orders, movements, fee tier, vault allowance. No state changes. **Default when authenticated** — a key alone grants no write authority. |
| `market-maker` | `read` + order placement / cancel / replace + the testnet faucet. Opt in to make markets. No fund movement. |
| `full` | `market-maker` + fund movement and privilege mutations — vault approve/deposit/withdraw and `transfer_rewards`. Opt-in only. |
The default `read` profile cannot mutate anything — order placement, cancel, replace, and close require `MONACO_CAPABILITY=market-maker` or higher. Fund-movement and privilege tools additionally require `full`; enable them by setting `MONACO_CAPABILITY=full`, or opt a single tool in with `MONACO_TOOL_ALLOWLIST` (e.g. `MONACO_TOOL_ALLOWLIST=vault_withdraw`). `MONACO_TOOL_DENYLIST` removes tools and wins over both the profile and the allowlist.
Also note: when a private key is configured, `MONACO_NETWORK` is **required** — the server refuses to default an authenticated session to `mainnet`.
Grant the `full` profile only when you intend an agent to move funds, and prefer a per-tool `MONACO_TOOL_ALLOWLIST` over blanket `full` — especially on `mainnet`.
## Delegated owner mode
Set `MONACO_DELEGATED_OWNER_USER_ID` to run the server on behalf of a delegated **owner** account instead of the configured wallet's own account. The wallet in `MONACO_PRIVATE_KEY` is the **agent**: on startup it authenticates as itself, then adopts an owner-scoped delegated session (`loginAsDelegatedOwner`). Every subsequent tool call — reads and trades — acts on the owner account, with the agent wallet preserved as the actor for audit. The owner must have registered this agent wallet with an active [delegation policy](/developers/delegated-agents); the server cannot self-assert ownership, and adoption fails if the agent is not authorized for that owner.
```json theme={null}
"env": {
"MONACO_NETWORK": "staging",
"MONACO_PRIVATE_KEY": "0x...",
"MONACO_CLIENT_ID": "your-client-id",
"MONACO_CAPABILITY": "market-maker",
"MONACO_DELEGATED_OWNER_USER_ID": "the-owner-user-id"
}
```
This is fail-closed: `MONACO_DELEGATED_OWNER_USER_ID` requires `MONACO_PRIVATE_KEY` and `MONACO_CLIENT_ID` (the agent must authenticate as itself first), a blank value is rejected, and if owner adoption fails the server does not silently fall back to trading as the agent's own account. The delegated session inherits withdrawal restrictions from the policy — fund-movement tools still require the `full` capability, and delegated sessions cannot withdraw owner collateral. Raw delegated-session creation is intentionally not exposed as a tool; owner adoption is a startup option, not an autonomous agent action.
## IDE setup
Follow the setup instructions below for your client. Some clients use an MCP config file; Claude Code can be configured via the CLI.
Claude Code registers MCP servers from the command line — no config file to edit.
**Public mode:**
```bash theme={null}
claude mcp add monaco \
-e MONACO_NETWORK=staging \
-- npx -y @0xmonaco/mcp-server
```
**Authenticated mode:**
For security, avoid pasting `MONACO_PRIVATE_KEY` into your shell history; prefer a secret manager or environment variable.
```bash theme={null}
claude mcp add monaco \
-e MONACO_NETWORK=staging \
-e MONACO_PRIVATE_KEY=0x... \
-e MONACO_CLIENT_ID=your-client-id \
-- npx -y @0xmonaco/mcp-server
```
Verify it registered with `claude mcp list`, then start a session with `claude`.
**Config file**: `~/Library/Application Support/Claude/claude_desktop_config.json`
**Public mode:**
```json theme={null}
{
"mcpServers": {
"monaco": {
"command": "npx",
"args": ["-y", "@0xmonaco/mcp-server"],
"env": {
"MONACO_NETWORK": "staging"
}
}
}
}
```
**Authenticated mode:**
```json theme={null}
{
"mcpServers": {
"monaco": {
"command": "npx",
"args": ["-y", "@0xmonaco/mcp-server"],
"env": {
"MONACO_NETWORK": "staging",
"MONACO_PRIVATE_KEY": "0x...",
"MONACO_CLIENT_ID": "your-client-id"
}
}
}
}
```
**Config file**: `%APPDATA%\Claude\claude_desktop_config.json`
**Public mode:**
```json theme={null}
{
"mcpServers": {
"monaco": {
"command": "npx",
"args": ["-y", "@0xmonaco/mcp-server"],
"env": {
"MONACO_NETWORK": "staging"
}
}
}
}
```
**Authenticated mode:**
```json theme={null}
{
"mcpServers": {
"monaco": {
"command": "npx",
"args": ["-y", "@0xmonaco/mcp-server"],
"env": {
"MONACO_NETWORK": "staging",
"MONACO_PRIVATE_KEY": "0x...",
"MONACO_CLIENT_ID": "your-client-id"
}
}
}
}
```
**Config file**: `~/.cursor/mcp.json`
**Public mode:**
```json theme={null}
{
"mcpServers": {
"monaco": {
"command": "npx",
"args": ["-y", "@0xmonaco/mcp-server"],
"env": {
"MONACO_NETWORK": "staging"
}
}
}
}
```
**Authenticated mode:**
```json theme={null}
{
"mcpServers": {
"monaco": {
"command": "npx",
"args": ["-y", "@0xmonaco/mcp-server"],
"env": {
"MONACO_NETWORK": "staging",
"MONACO_PRIVATE_KEY": "0x...",
"MONACO_CLIENT_ID": "your-client-id"
}
}
}
}
```
**Config file**: `~/.codeium/windsurf/mcp_config.json`
**Public mode:**
```json theme={null}
{
"mcpServers": {
"monaco": {
"command": "npx",
"args": ["-y", "@0xmonaco/mcp-server"],
"env": {
"MONACO_NETWORK": "staging"
}
}
}
}
```
**Authenticated mode:**
```json theme={null}
{
"mcpServers": {
"monaco": {
"command": "npx",
"args": ["-y", "@0xmonaco/mcp-server"],
"env": {
"MONACO_NETWORK": "staging",
"MONACO_PRIVATE_KEY": "0x...",
"MONACO_CLIENT_ID": "your-client-id"
}
}
}
}
```
After editing the config file, restart your AI assistant.
Never commit your MCP config file to version control. `MONACO_PRIVATE_KEY` is a raw private key — anyone with access to it controls your wallet.
## Configuration
Network to connect to (`mainnet`, `development`, `staging`, `local`). Set to `staging` for testnet. **Required when `MONACO_PRIVATE_KEY` is set** — the server refuses to default an authenticated session to `mainnet`. Public-only mode defaults to `mainnet`.
Your wallet's private key in `0x...` format. Required for authenticated tools. If omitted, only public tools are available.
Your Monaco application client ID. Required when `MONACO_PRIVATE_KEY` is set. Contact [team@0xmonaco.com](mailto:team@0xmonaco.com) to obtain one.
Capability profile for authenticated mode: `read`, `market-maker`, or `full`. See [capability profiles](#capability-profiles). Defaults to `read` (no write authority); `market-maker` adds order management, and `full` adds fund movement.
Comma-separated tool names to enable on top of the active profile — an explicit per-tool opt-in, including for `full`-only fund-movement tools (e.g. `vault_withdraw`).
Comma-separated tool names to disable. Wins over both the profile and the allowlist.
Sei RPC endpoint URL. Defaults to `https://evm-rpc-testnet.sei-apis.com` when `MONACO_NETWORK=staging`. The default is a public testnet RPC — rate limits may vary and it is not suitable for production. Override with a dedicated private RPC provider for better reliability.
Run the server on behalf of a delegated **owner** account instead of the configured wallet's own account. The `MONACO_PRIVATE_KEY` wallet is the agent: on startup it authenticates as itself, then adopts an owner-scoped session (`loginAsDelegatedOwner`) so every tool call acts on the owner, with the agent wallet preserved as the audit actor. Fail-closed — requires `MONACO_PRIVATE_KEY` and `MONACO_CLIENT_ID`, rejects a blank value, and never falls back to trading as the agent's own account. See [delegated owner mode](#delegated-owner-mode).
## Available tools
Public tools require no credentials. Authenticated tools require `MONACO_PRIVATE_KEY`, `MONACO_CLIENT_ID`, and `MONACO_NETWORK`, and are further gated by the active [capability profile](#capability-profiles) — the profile column below shows the minimum profile each tool needs.
### Search
| Tool | Profile | Description |
| - | - | - |
| `search_docs` | Public | Search the Monaco documentation for protocol, SDK, trading, and development guides. |
### Market
| Tool | Profile | Description |
| - | - | - |
| `get_trading_pairs` | Public | List trading pairs with optional filters (market type, base/quote token, active status). |
| `get_trading_pair_by_symbol` | Public | Look up a trading pair by symbol (e.g. `BTC/USDC`). |
| `get_candlesticks` | Public | Fetch OHLCV candlestick data for a trading pair. |
| `get_market_metadata` | Public | Get tick size, minimum order size, and trading rules for a pair. |
| `get_screener` | Public | Screen trading pairs with market stats (price, volume, change), filtered by market type, category, and active status. |
| `get_market_stats` | Public | Get exchange-wide life-to-date cumulative volume and trade count across all markets. |
### Perp market data
| Tool | Profile | Description |
| - | - | - |
| `get_perp_market_config` | Public | Get a perpetual market's configuration (leverage limits, margin requirements, funding parameters). |
| `get_perp_market_summary` | Public | Get a perpetual market's live summary (mark/index price, funding rate, open interest, 24h stats). |
| `get_mark_price` | Public | Get the current mark price for a perpetual market. |
| `get_index_price` | Public | Get the current index (oracle) price for a perpetual market. |
| `get_funding_state` | Public | Get the current funding rate and next funding time for a perpetual market. |
| `get_open_interest` | Public | Get the current open interest for a perpetual market. |
| `get_funding_history` | Public | List historical funding-rate records for a perpetual market. |
| `get_all_funding_history` | Public | List historical funding-rate records across all perpetual markets. |
### Orderbook
| Tool | Profile | Description |
| - | - | - |
| `get_orderbook` | Public | Fetch a current orderbook snapshot with bids and asks. |
### Trades
| Tool | Profile | Description |
| - | - | - |
| `get_trades` | Public | Fetch historical trades for a trading pair. |
### Trading
Order reads (`get_orders`, `get_order`) are available under `read`; the mutations below require `market-maker` or higher.
| Tool | Profile | Description |
| - | - | - |
| `place_limit_order` | `market-maker` | Place a limit order with price, quantity, and time-in-force. The SDK supplies an idempotency key if omitted. Retain an explicit key to retry the same submission within 24 hours of acceptance. Calling again without the original key is not idempotent; never retry an ambiguous failure with a new key. If the original key is unavailable, report the ambiguity. |
| `place_market_order` | `market-maker` | Place a market order — executes immediately inside a mandatory 1,000 bps protective price band, so it may partially fill or be rejected. The SDK supplies an idempotency key if omitted. Retain an explicit key to retry the same submission within 24 hours of acceptance. Calling again without the original key is not idempotent; never retry an ambiguous failure with a new key. If the original key is unavailable, report the ambiguity. |
| `replace_order` | `market-maker` | Modify an existing order's price and/or quantity. Not idempotent — an ambiguous failure (timeout/network error/5xx with no order) cannot be resolved from inside MCP, which has no order-stream tool; never retry, and report the ambiguity instead. |
| `cancel_order` | `market-maker` | Cancel a single order by ID. |
| `batch_cancel_orders` | `market-maker` | Cancel multiple orders by ID (non-atomic — a failed leg does not stop the others). |
| `batch_cancel_all_orders` | `market-maker` | Cancel all open orders, optionally filtered to a trading pair (unfiltered, it cancels your entire open-order book). |
| `batch_close_all_positions` | `market-maker` | Close all open perpetual positions with market reduce-only orders (best-effort, optional pair filter). |
| `get_orders` | `read` | List your orders with optional filters. |
| `get_order` | `read` | Get details for a specific order by ID. |
### Positions
Position reads are available under `read`; `close_position` requires `market-maker`, and margin add/reduce move collateral and require `full`.
| Tool | Profile | Description |
| - | - | - |
| `get_positions` | `read` | List your perpetual positions with optional filters (margin account, trading pair, status). |
| `get_position` | `read` | Get details for a specific perpetual position by ID. |
| `get_position_risk` | `read` | Get liquidation price, margin ratio, and risk metrics for a perpetual position. |
| `get_position_history` | `read` | List historical (closed/reduced) perpetual position records with optional filters. |
| `get_position_pnl_history` | `read` | Get the PnL history time series for a perpetual position over an interval. |
| `close_position` | `market-maker` | Close a perpetual position with a market, limit, or IOC reduce order. |
### Portfolio
| Tool | Profile | Description |
| - | - | - |
| `get_portfolio_summary` | `read` | Get your authoritative portfolio summary (equity, PnL, exposure) over a period. |
| `get_portfolio_performance` | `read` | Get your portfolio performance time series over a period. |
| `get_portfolio_realized_pnl` | `read` | Get your realized PnL breakdown over a period. |
| `get_portfolio_assets` | `read` | Get your per-asset portfolio allocation and balances over a period. |
| `get_portfolio_margin` | `read` | Get your portfolio margin overview (collateral, maintenance margin, health). |
### Margin accounts
Reads and risk simulations are available under `read`; collateral transfers move funds and require the `full` profile (or an explicit allowlist entry).
| Tool | Profile | Description |
| - | - | - |
| `get_margin_accounts` | `read` | List your margin accounts with optional filters (state, trading pair). |
| `get_parent_margin_account_summary` | `read` | Get your parent (cross) margin account summary. |
| `get_available_collateral` | `read` | Get available (unlocked) collateral for allocating to margin accounts or risk buckets. |
| `get_parent_margin_account_movements` | `read` | List collateral movements for your parent (cross) margin account. |
| `simulate_parent_margin_order_risk` | `read` | Preview the risk impact of a prospective order against your parent (cross) margin account (read-only simulation). |
| `simulate_risk_bucket_order_risk` | `read` | Preview the risk impact of a prospective order for a new or existing risk bucket (read-only simulation). |
| `transfer_collateral_to_parent_margin_account` | `full` | Move collateral from spot into your parent (cross) margin account. |
| `transfer_collateral_from_parent_margin_account` | `full` | Move collateral out of your parent (cross) margin account back to spot. |
| `transfer_collateral_to_risk_bucket` | `full` | Allocate collateral into an isolated or cross risk bucket. |
| `transfer_collateral_to_sub_account` | `full` | Move collateral from your default (parent) margin account into one of your margin sub-accounts. |
| `transfer_collateral_from_sub_account` | `full` | Move collateral from one of your margin sub-accounts back to your default (parent) margin account. |
### Profile
| Tool | Profile | Description |
| - | - | - |
| `get_profile` | `read` | Fetch your core profile (address, username, account type, withdrawal status). |
| `get_balances` | `read` | List token balances with pagination. |
| `get_balance_by_asset` | `read` | Get balance for a specific asset. |
| `get_movements` | `read` | Fetch transaction and movement history with optional filters. |
| `get_user_trades` | `read` | List your own trade fills with optional filters and cursor pagination. |
| `get_funding_payments` | `read` | List your perpetual funding payments with optional filters (trading pair, position, margin account). |
### Fees
| Tool | Profile | Description |
| - | - | - |
| `simulate_fees` | `read` | Simulate fees for an order before placing it. |
| `get_my_fee_tier` | `read` | Get your current fee tier, rolling 14-day volumes, and a pair's fee schedule. |
### Vault
Fund-movement tools require the `full` profile (or an explicit allowlist entry); the allowance and pending-withdrawal reads are available under `read`.
| Tool | Profile | Description |
| - | - | - |
| `vault_approve` | `full` | Approve the vault contract to spend a token. |
| `vault_deposit` | `full` | Deposit tokens into the vault (optional `target`: `spot` default, or `margin` to route into collateral). |
| `vault_withdraw` | `full` | Withdraw tokens from the vault. |
| `vault_get_allowance` | `read` | Check current token allowance for the vault. |
| `vault_needs_approval` | `read` | Check whether an approval transaction is needed before depositing. |
| `get_pending_withdrawals` | `read` | List your pending withdrawals still awaiting on-chain root confirmation. |
### Faucet
Testnet only — the faucet tools are never registered on `mainnet`.
| Tool | Profile | Description |
| - | - | - |
| `request_faucet` | `market-maker` (non-mainnet) | Request test tokens for all trading pairs. |
| `get_faucet_info` | `read` (non-mainnet) | Check remaining faucet quota, next-request time, and recent mint history. |
### Trader codes & rewards
| Tool | Profile | Description |
| - | - | - |
| `get_trader_code_info` | Public | Look up a trader code's details (no credentials required). |
| `check_trader_code_availability` | Public | Public advisory check of whether a custom PitPass TraderCode handle is available (no credentials required). |
| `get_my_trader_code` | `read` | Fetch your own trader code. |
| `set_trader_code` | `market-maker` | Claim or edit your custom (vanity) PitPass TraderCode handle. |
| `clear_trader_code` | `market-maker` | Clear your custom PitPass TraderCode handle, reverting to the wallet-derived default. |
| `get_rewards_balance` | `read` | Fetch your TraderCodes rewards-bucket balances across reward tokens. |
| `transfer_rewards` | `full` | Move your earned TraderCodes rewards from the rewards bucket into your own tradeable balance (ledger-only). |
### Copy trading
The leaderboard, leader profiles and closed trades are public. Your own reads are available under `read`; registering as a leader and stopping a follow require `market-maker`; following (which moves the allocation into a copy account) and adding margin to a copy position (which moves copy account collateral into the position) require `full`.
| Tool | Profile | Description |
| - | - | - |
| `list_lead_traders` | Public | Public copy trading leaderboard: lead traders ranked by ROI, PnL, followers, allocated AUM, marked AUM or win rate over a stats window (no credentials required). |
| `get_lead_trader` | Public | Public profile, follow summary and stats of one lead trader, by custom TraderCode handle (no credentials required). |
| `list_lead_trader_closed_trades` | Public | Public list of a lead trader's closed trades, newest first (no credentials required). |
| `list_lead_trader_open_positions` | `read` | Open positions of a lead trader you currently follow (followers only). |
| `get_my_lead_trader` | `read` | Fetch your own lead trader profile, follow summary and stats. |
| `list_my_followers` | `read` | List your followers, as a lead trader. |
| `list_my_follows` | `read` | List your copy trading follows and their settings. |
| `get_follow` | `read` | Fetch one of your follows with its most recent skipped mirrors and its copy account's marked equity. |
| `preview_follow` | `read` | Preview what following a lead trader with copyExisting would open: estimated margin and fees and how many positions would be copied (read-only). |
| `list_follow_leader_positions` | `read` | Open positions of the lead trader one of your follows copies (live follows only). |
| `upsert_lead_trader` | `market-maker` | Register as a lead trader, or edit your lead profile (profit share, bio, status, minimum allocation). |
| `upsert_follow` | `full` | Follow a lead trader (moves the allocation into a new copy account) or edit one of your follows. |
| `stop_follow` | `market-maker` | Stop one of your follows: the leader's fills stop being mirrored. |
| `close_follow_positions` | `market-maker` | Close one follow's copy positions at market and keep the follow (it keeps mirroring). |
| `close_all_copy_positions` | `market-maker` | Close the copy positions of all your follows at market and keep the follows. |
| `add_follow_position_margin` | `full` | Add margin to one of your copy positions from the follow's copy account (lowers its leverage; your margin account is never touched; no reduction). Not idempotent — after a timeout, read the follow before retrying, or the amount may be added twice. |
### Market pulse
| Tool | Profile | Description |
| - | - | - |
| `get_pulse` | Public | Get the latest market-pulse news feed: AI-synthesized, per-asset headlines with sentiment tags. |
## Example prompts
Once the server is connected, drive Monaco entirely in natural language. Your AI assistant picks the right tools and chains them together.
**Public — no credentials needed:**
> What's the current ETH/USDC orderbook on Monaco, and how does the mid price compare to the last few trades?
> Pull the last 24 hours of hourly candlesticks for BTC/USDC and summarize the trend.
**Authenticated — requires `MONACO_PRIVATE_KEY`, `MONACO_CLIENT_ID`, and `MONACO_NETWORK` (and `MONACO_CAPABILITY=market-maker` to place orders):**
> Request test tokens from the faucet, then place a limit buy for 0.1 ETH/USDC at 5% below the current mid price.
> Check my open orders and balances, then cancel any resting buy orders older than I'd expect to still be unfilled.
> Simulate the fees for a 1 ETH/USDC market sell before I place it.
In authenticated mode the agent can place and cancel real orders on the configured network. Start on `staging` (testnet) and review what the agent proposes before confirming trades.
# Python, Go, C++ & More
Source: https://docs.0xmonaco.com/sdk/python-quickstart
Use Python directly, or generate typed clients from Monaco's Buf Schema Registry module
Use this page when the TypeScript, React, or Rust SDKs are not the right fit. You have two integration paths:
| Path | Use when |
| - | - |
| Python REST | You want a direct Python backend integration over HTTP/JSON. |
| Generated clients | You want typed gRPC clients for Go, Python, C++, Java, C#, Kotlin, Swift, Ruby, PHP, TypeScript, or another Buf-supported target. |
## Python REST
This example uses Python with Monaco's REST API. Public market data does not require authentication; trading and account routes require a Monaco client ID plus a wallet-authorized ed25519 session key.
### Prerequisites
```bash theme={null}
pip install requests eth-account eth-utils pynacl
```
You'll need a secp256k1 private key for an Ethereum-compatible wallet and a Monaco `clientId`.
### Market Data
No authentication is required to read trading pairs.
```python theme={null}
import requests
BASE_URL = "https://staging.apimonaco.xyz"
API_PREFIX = "/api"
CLIENT_ID = "YOUR_CLIENT_ID"
session = requests.Session()
pairs = session.get(
f"{BASE_URL}{API_PREFIX}/market/pairs",
params={"isActive": "true", "pageSize": 20},
).json()["tradingPairs"]
for pair in pairs[:5]:
print(f"{pair['symbol']} tick={pair['tickSize']} min={pair['minOrderSize']}")
pair = next(pair for pair in pairs if pair["symbol"] == "SEI/USDC")
pair_id = pair["id"]
```
### Authenticate
Monaco authenticates with a wallet-authorized ed25519 **session key**. Generate a keypair locally, have the wallet sign a challenge that commits to it, then sign every authenticated request with the session key. No tokens are issued.
```python theme={null}
from eth_account import Account
from eth_account.messages import encode_defunct
from eth_utils import to_hex
from nacl.signing import SigningKey
PRIVATE_KEY = "0xYOUR_PRIVATE_KEY"
acct = Account.from_key(PRIVATE_KEY)
address = acct.address
session_key = SigningKey.generate()
sessionPublicKey = session_key.verify_key.encode().hex()
challenge = session.post(
f"{BASE_URL}{API_PREFIX}/auth/challenge",
json={
"address": address,
"clientId": CLIENT_ID,
"sessionPublicKey": sessionPublicKey,
},
).json()
sig = Account.sign_message(
encode_defunct(text=challenge["message"]),
private_key=PRIVATE_KEY,
)
signature = to_hex(bytes(sig.signature))
session.post(
f"{BASE_URL}{API_PREFIX}/auth/verify",
json={
"address": address,
"signature": signature,
"nonce": challenge["nonce"],
"clientId": CLIENT_ID,
"sessionPublicKey": sessionPublicKey,
},
).raise_for_status()
```
### Sign Requests
This guide uses legacy signing, where every authenticated request carries `X-Monaco-PublicKey`, `X-Monaco-Timestamp`, and `X-Monaco-Signature`, and the signature covers `METHOD\npath?query\ntimestamp_ms\nSHA256_hex(body)`. Monaco also accepts [RFC 9421 HTTP message signatures](/reference/http-message-signatures) over the same session key; a Python client can implement either, but never both on one request. Serialize the body yourself so the bytes you hash are exactly the bytes you send:
```python theme={null}
import hashlib
import json
import time
def signed_request(method, path, body_obj=None):
request_path = f"{API_PREFIX}{path}"
body = json.dumps(body_obj).encode() if body_obj is not None else b""
timestamp_ms = str(int(time.time() * 1000))
body_hash = hashlib.sha256(body).hexdigest()
signing_string = f"{method}\n{request_path}\n{timestamp_ms}\n{body_hash}"
signature = session_key.sign(signing_string.encode()).signature.hex()
headers = {
"X-Monaco-PublicKey": sessionPublicKey,
"X-Monaco-Timestamp": timestamp_ms,
"X-Monaco-Signature": signature,
"Content-Type": "application/json",
}
return session.request(method, f"{BASE_URL}{request_path}", data=body, headers=headers)
```
### Place and Cancel an Order
The API keeps `idempotencyKey` optional. Raw REST callers and generated Python bindings must supply their own keys for retry protection. Create and retain the key before serialization or signing. Persist the prepared payload and key before sending if recovery must survive a process restart.
```python theme={null}
from uuid import uuid4
order_request = {
"idempotencyKey": str(uuid4()),
"tradingPairId": pair_id,
"orderType": "LIMIT",
"side": "BUY",
"price": "0.0100",
"quantity": "0.1",
"timeInForce": "GTC",
}
# Save order_request before sending; reuse it unchanged for recovery.
order = signed_request("POST", "/orders", order_request).json()
orderId = order["orderId"]
print(f"order {orderId}: {order['status']}")
cancel = signed_request("POST", "/orders/cancel", {"orderId": orderId}).json()
print(f"cancel: {cancel['status']}")
```
For a lost create response, pass the same `order_request` to `signed_request` again, which produces a fresh signature. Use a distinct key for every batch-create item, generated before signing the batch. Generated protobuf clients use the field name `idempotency_key`; populate it before `SerializeToString()` and retain the message for recovery. Protection lasts 24 hours after acceptance and retries do not extend it. Do not retry an unresolved submission after expiry. These examples do not retry automatically.
A `GTC` order rests on the book until filled or cancelled. See [Order Management](/developers/trade) for other time-in-force options.
## Generate gRPC Clients with BSR
Monaco publishes protobuf definitions to the [Buf Schema Registry](https://buf.build/monaco-sdk/api). Use BSR when you want generated gRPC clients in Go, Python, C++, Java, C#, Kotlin, Swift, Ruby, PHP, TypeScript, or another supported language.
### Install Buf
```bash theme={null}
brew install bufbuild/buf/buf
# or: curl -sSL https://buf.build/install.sh | sh
```
### Create `buf.gen.yaml`
Use the Monaco BSR module as the input and select plugins for your target language.
```yaml Go theme={null}
version: v2
inputs:
- module: buf.build/monaco-sdk/api:main
plugins:
- remote: buf.build/protocolbuffers/go
out: gen/go
opt:
- paths=source_relative
- remote: buf.build/grpc/go
out: gen/go
opt:
- paths=source_relative
```
```yaml Python theme={null}
version: v2
inputs:
- module: buf.build/monaco-sdk/api:main
plugins:
- remote: buf.build/protocolbuffers/python
out: gen/python
- remote: buf.build/grpc/python
out: gen/python
```
```yaml C++ theme={null}
version: v2
inputs:
- module: buf.build/monaco-sdk/api:main
plugins:
- remote: buf.build/protocolbuffers/cpp
out: gen/cpp
- remote: buf.build/grpc/cpp
out: gen/cpp
```
```yaml TypeScript theme={null}
version: v2
inputs:
- module: buf.build/monaco-sdk/api:main
plugins:
- remote: buf.build/bufbuild/es
out: gen/ts
- remote: buf.build/connectrpc/es
out: gen/ts
```
Browse the [BSR plugin catalog](https://buf.build/plugins) for other targets and swap the remote plugins into the same shape.
### Generate
```bash theme={null}
buf generate
```
The generated code is written to the `out:` directories you configured.
## REST Clients from OpenAPI
If you prefer REST in a language other than Python, generate a client from Monaco's OpenAPI spec. The spec is generated from the same protobuf contracts and powers the [REST API Reference](/api-reference), which is the source of truth for routes, request bodies, responses, and authentication headers.
# useAuth
Source: https://docs.0xmonaco.com/sdk/react/hooks/use-auth
Wallet-based authentication with state management
```tsx theme={null}
import { useAuth } from "@0xmonaco/react";
const {
authenticationStatus,
login,
logout,
refreshAuth,
refreshSession,
revokeSession,
authenticate,
signChallenge,
createChallenge
} = useAuth();
```
`useAuth` wraps the SDK's wallet-authorized session-key flow. `login()` generates a session key, prompts the wallet once, and (via the provider) persists the session; every subsequent request is signed automatically. There are no access or refresh tokens. See [TypeScript authentication](/sdk/typescript/auth) for the lower-level SDK model.
## Authentication State
Current authentication state:
* `"unauthenticated"` - Not logged in
* `"authenticating"` - Login in progress
* `"authenticated"` - Session active
## Core Actions
Authenticate the user with a wallet signature, using the address and `clientId` from context. Returns an `AuthState` (with the session keypair). Takes no arguments — the wallet and `clientId` come from the provider.
WebSocket channels are connected separately; `login()` does not open any connection. Public channels (OHLCV, Orderbook, Trades) don't require authentication.
```tsx theme={null}
const authState = await login();
```
Revoke the session on the server and clear local auth state.
Extend the current session's expiry. The keypair is unchanged; returns the updated `AuthState`.
## Session Actions
Lower-level refresh that returns just the new `expiresAt`. `refreshAuth()` wraps this and updates the stored `AuthState`.
Revoke the current session on the server without clearing local React state (`logout()` does both).
## Advanced Building Blocks
`login` / `authenticate` run these internally and manage the session keypair for you. Use them directly only for custom flows.
The programmatic equivalent of `login()` — generates the session key, prompts the wallet, verifies, and returns the `AuthState`.
Ask the connected wallet to authorize a challenge message. Returns a hex signature — EIP-191 from an EOA, or an EIP-1271 contract signature from a smart-contract wallet such as a Safe. Submit it unchanged; do not assume the 65-byte EOA shape. Server-side EIP-1271 verification is deliberately narrow: it is offered only to a wallet holding a BuilderCodes payout bucket, on a deployment with the contract-wallet flag on and a chain RPC endpoint configured. An ordinary smart-contract trading account is NOT covered — it still takes the recovery path and cannot authenticate.
Create a single-use challenge whose message embeds the given session public key. Returns `nonce`, `message`, and `expiresAt`.
## Example: Login Button
```tsx theme={null}
import { useAuth } from "@0xmonaco/react";
function LoginButton() {
const { login, authenticationStatus } = useAuth();
const handleLogin = async () => {
try {
const authState = await login();
console.log('Logged in:', authState.user);
} catch (error) {
if (error.message.includes('SDK not available')) {
alert('SDK is still loading, please wait');
} else if (error.message.includes('Wallet not connected')) {
alert('Please connect your wallet first');
} else {
console.error('Login failed:', error);
}
}
};
return (
);
}
```
Auth actions validate SDK availability, wallet connection, and required parameters before running, throwing a descriptive `Error` (e.g. `"SDK not available"`, `"Wallet not connected"`, `"Client ID is required"`) when a precondition is missing.
# useCopyTrading
Source: https://docs.0xmonaco.com/sdk/react/hooks/use-copy-trading
Copy trading reads and writes, and live follow state via the copy WebSocket channel
```tsx theme={null}
import { useCopyEvents, useCopyTrading } from "@0xmonaco/react";
const {
listLeadTraders,
getLeadTrader,
listLeadTraderClosedTrades,
listLeadTraderOpenPositions,
upsertFollow,
stopFollow,
closeFollowPositions,
closeAllCopyPositions,
listMyFollows,
getFollow,
listFollowLeaderPositions,
previewFollow,
addFollowPositionMargin,
upsertLeadTrader,
getMyLeadTrader,
listMyFollowers,
} = useCopyTrading();
const { relationships, relationshipsById, recentSkips, subscribed, error, clearError } = useCopyEvents();
```
For concepts see [Copy Trading](/developers/copy-trading).
## useCopyTrading
Returns the sixteen [`sdk.copyTrading`](/sdk/typescript/copy-trading) methods with the same signatures. Each throws `SDK not available` until the provider has built the SDK. The leaderboard, `getLeadTrader` and `listLeadTraderClosedTrades` need no session; every other method needs an authenticated one.
```tsx theme={null}
function Leaderboard() {
const { listLeadTraders, upsertFollow } = useCopyTrading();
const [leaders, setLeaders] = useState([]);
useEffect(() => {
listLeadTraders({ sort: "roi", window: "30d" }).then((page) => setLeaders(page.leaders));
}, [listLeadTraders]);
const follow = (handle: string) => upsertFollow({ leader: handle, allocation: "1000", slPct: "0.25" });
// ...
}
```
An `upsertFollow` edit replaces the follow's settings: every optional setting left out reverts to its default. See [upsertFollow](/sdk/typescript/copy-trading).
`addFollowPositionMargin({ followId, tradingPairId, amount })` adds margin to one of your isolated copy positions, paid from the follow's copy account (your margin account is never touched). It returns the position's margin after the add (`positionMargin`) and what the copy account can still put in (`copyAccountAvailable`). There is no reduction; a stopping or stopped follow is refused (409). See [addFollowPositionMargin](/sdk/typescript/copy-trading).
## useCopyEvents
A typed subscription to the authenticated [`copy` channel](/sdk/typescript/websockets#copy-channel), wrapping `sdk.ws.copy()`. It streams to the **follower** only, and runs only while a user is signed in: it subscribes on login, and logout or an account switch clears the follows and skips before the next user's snapshot arrives.
On subscribe the channel delivers a [snapshot](/sdk/typescript/websockets#subscribe-time-snapshots) of your live follows (`ACTIVE` or `CLOSE_ONLY`). Each `copy_relationship_update` frame then carries a follow's full state and replaces its entry, unless its `version` is older than the stored one (a late frame). A follow stopped while the socket is up keeps its entry with status `STOPPED`. Each later snapshot (after a reconnect) replaces the whole set, so a follow that stopped while the socket was down leaves it.
`copy_skip` frames collect in `recentSkips`, newest first, up to `MAX_RECENT_COPY_SKIPS` (50). The snapshot carries no skips; read `getFollow(followId)` for a follow's persisted recent skips.
`cumNetRealized` and `hwm` on each follow are its profit-share basis, not the copy account's balance.
The subscription is torn down on unmount and re-established when `sdk.ws` or the signed-in user changes.
## Returns
Your follows as last reported, newest first by `startedAt`.
The same entries keyed by `relationshipId` (the follow id, `Follow.followId` over REST).
Leader fills your follows did not mirror while subscribed, newest first, capped at 50.
Whether the `copy` subscription is registered. Not a connection indicator.
A rejected subscription request. Connection loss and malformed frames never appear here.
Clear `error`.
# useFees
Source: https://docs.0xmonaco.com/sdk/react/hooks/use-fees
Simulate order fees before placing trades
```tsx theme={null}
import { useFees } from "@0xmonaco/react";
const { simulateFees, getMyFeeTier } = useFees();
```
## Methods
Calculate exact fees for an order before placing it. Requires authentication.
**Parameters:**
* `tradingPairId`: string - Trading pair UUID
* `side`: "BUY" | "SELL" - Order side
* `price`: string - Price per unit
* `quantity`: string - Quantity to trade
* `orderType`: "LIMIT" | "MARKET" (optional) - Defaults to LIMIT behaviour if omitted. Pass `"MARKET"` to get a slippage-adjusted `buyOrderLockAmount`
* `slippageToleranceBps`: number (optional, 0–1000) - Slippage tolerance in basis points. Only valid with `orderType: "MARKET"`. Default: `1000` (10%), matching the protective price band
**Returns:**
* `notional`: string - Total order value (price × quantity)
* `monacoTakerFee`: string - Monaco protocol taker fee
* `monacoTakerFeeBpsExact`: string | null - Tier-resolved Monaco taker rate as a decimal bps string (e.g. `"6.5"`) — the exact rate the ledger charges this caller, equal to `monacoTakerFee / notional`. Prefer this over the integer field below
* `monacoMakerRebateBpsExact`: string | null - Tier-resolved Monaco maker rebate as a decimal bps string (negative = rebate, e.g. `"-1"`) — the exact rate the ledger credits this caller
* `monacoTakerFeeBps`: number - **Deprecated.** Whole-bps taker rate; an integer cannot carry fractional tiered rates (e.g. 6.5), so this echoes the flat trading-pair column, not the tier-resolved rate. Use `monacoTakerFeeBpsExact` or `getMyFeeTier`
* `monacoMakerRebate`: string - Monaco maker rebate (negative = rebate)
* `monacoMakerRebateBps`: number - **Deprecated.** Whole-bps maker rebate; same integer limitation as above. Use `monacoMakerRebateBpsExact` or `getMyFeeTier`
* `applicationTakerFee`: string - Application taker fee
* `applicationTakerFeeBps`: number - Application fee in basis points
* `applicationName`: string | null - Application name (if applicable)
* `totalTakerFees`: string - Total fees for taker (monaco + application)
* `takerTotalPayment`: string - Total amount taker must pay
* `makerTotalReceipt`: string - Total amount maker receives
* `buyOrderLockAmount`: string | null - Amount to lock for BUY orders. For MARKET orders includes slippage buffer
* `maxQuantity`: string | null - Maximum quantity affordable at the given price, after fees. Use this for the 100% balance slider
* `maxQuantityRaw`: string | null - Same value in raw (smallest unit) format
* `slippageToleranceBps`: number | null - Slippage tolerance used in the calculation, echoed back. `null` for LIMIT orders
Look up your current fee tier, rolling 14-day volumes, and a pair's six-row fee schedule. Requires authentication. The tier and volumes are wallet-wide: they sum the wallet's trades across every application it trades through, so they read the same from any application.
**Parameters:**
* `tradingPairId`: string - Trading pair UUID whose fee schedule to return
**Returns:**
* `currentTierLevel`: number - Resolved tier, `1` (lowest volume) through `6` (highest), from the higher of `weightedVolume14d` and any volume floor set on the wallet (a floor that is set or changed counts once it applies to the wallet's fills, usually within a minute, and likewise for an account the wallet opens in a new application; it stops counting at once when removed or expired)
* `weightedVolume14d`: string - Weighted 14-day volume (perp + 2.5× spot) the tier resolves from, unless a higher volume floor is set on the wallet
* `spotVolume14d`: string - Rolling 14-day spot volume
* `perpVolume14d`: string - Rolling 14-day perp volume
* `volumeToNextTier`: string | null - Additional weighted volume needed to reach the next tier, counted from the volume the current tier resolved from; omitted at tier 6
* `nextTierLevel`: number | null - Next tier level; omitted at tier 6
* `feeSchedule`: FeeTierScheduleRow\[] - The pair's six rows, ascending by tier. Each row: `tierLevel` (number), `minVolumeThreshold` (string), `makerFeeBps` (string — human bps, negative = rebate), `takerFeeBps` (string — human bps)
## Example
```tsx theme={null}
import { useFees, useMarket } from "@0xmonaco/react";
import { useState, useEffect } from "react";
function FeeCalculator() {
const { getTradingPairBySymbol } = useMarket();
const { simulateFees } = useFees();
const [fees, setFees] = useState(null);
const calculateFees = async () => {
const pair = await getTradingPairBySymbol('BTC/USDC');
// LIMIT order — exact fee calculation
const result = await simulateFees({
tradingPairId: pair.id,
side: "BUY",
price: "50000",
quantity: "0.1"
});
setFees(result);
};
return (
{fees && (
Order Value: ${fees.notional}
Total Fees: ${fees.totalTakerFees}
You Pay: ${fees.takerTotalPayment}
{/* 100% slider — use maxQuantity instead of balance / price */}
Max quantity you can buy: {fees.maxQuantity}
)}
);
}
```
## Fee Breakdown
Monaco uses a maker-taker fee model:
| Role | Fee Type | Description |
| - | - | - |
| **Taker** | Positive fee | Pay fees when taking liquidity (market orders, crossing spread) |
| **Maker** | Rebate | Earn rebates when providing liquidity (limit orders in book) |
Use `simulateFees` before placing orders to show users exact costs upfront.
# useInstruments
Source: https://docs.0xmonaco.com/sdk/react/hooks/use-instruments
Live market configuration via the instrument WebSocket channel
```tsx theme={null}
import { useInstruments } from "@0xmonaco/react";
const {
instruments,
instrumentsByPair,
subscribed,
error,
clearError,
} = useInstruments();
```
A typed subscription to the public [`instrument` channel](/sdk/typescript/websockets#instruments-channel), wrapping [`sdk.ws.instruments()`](/sdk/typescript/websockets#instruments-channel). No authentication is required.
On subscribe the channel delivers a [snapshot](/sdk/typescript/websockets#subscribe-time-snapshots) of every listed market (inactive ones flagged `isActive: false`), then live change frames. Every frame carries a market's **full current configuration** rather than a delta, so the hook keeps one terminal-state entry per trading pair: each live event replaces that market's entry outright, and each snapshot **replaces the whole map** — it is the authoritative market set at the moment it was taken.
Three `change` kinds are emitted today: `listing` (a market became tradable), `delisting` (deactivated), and `config_change`. `config_change` covers a narrow set of edits — tick size, quantity step, order-size bounds, category, and a perp market's margin ladder (`riskTiers`, with `changedFields` naming `risk_tiers`). An edit to fees emits nothing. `halt` and `unhalt` are defined on `InstrumentChangeKind` for wire-forward-compatibility with a future market-regime producer and are not emitted by the admin trading-pair path — keep a `default` branch, but do not build behavior on them yet. The subscription is torn down on unmount, and re-established when `sdk.ws` or `tradingPairId` changes.
Detected broadcast lag closes the entire socket with code `1013`, including an instrument-only connection. A dropped update may belong to a market that never emits again, so continuing from the retained tail cannot repair every key. With automatic reconnect enabled, the core client resubscribes and this hook replaces its market set when a fresh instrument snapshot arrives.
A reconnect request or `onResync` callback is not proof that the snapshot succeeded. If the snapshot is unavailable, or a decision requires independently refreshed configuration, reconcile over REST. No single endpoint carries every field this channel does, so an unfiltered hook requires multiple requests:
* `sdk.market.getPaginatedTradingPairs()`, walking `page` until you have every market (one page can omit some), covers `isActive`, `tickSize`, `quantityStepSize`, `minOrderSize`, `maxOrderSize`, `category`, `marketType`, and `maxLeverage`.
* `sdk.market.getPerpMarketConfig(tradingPairId)` covers the two remaining perp fields, which the pair list does not carry — and note the rename: this channel's `initialMarginRate` / `maintenanceMarginRate` are `initialMarginRatio` / `maintenanceMarginRatio` there. It is scoped to one pair, so following every market costs one request per perp market.
That cost is why this is a deliberate reconcile — on a suspected gap, or when a decision depends on the values — rather than something to run routinely.
Each `useInstruments` call keeps its own state, so mounting the hook in two components subscribes twice. If both resolve to the **same** channel — both omitting `tradingPairId`, or both passing the same id — the second joins a channel the client already holds, which the server does not re-snapshot; the core helper replays its cached baseline to it instead. Mounts with different filters are distinct subscriptions and each get their own server snapshot. Either way the baseline arrives asynchronously, like a real snapshot, so `instruments` is empty on the first render — render an empty list, not a spinner keyed on `subscribed`.
## Parameters
Optional trading pair UUID to filter on. Omit it to follow every market. An empty string is rejected by the underlying core helper and surfaces through `error`.
## Return Values
Current configuration of every market seen on the channel, ordered by `symbol` and then by `tradingPairId` — a spot and a perp market sharing a symbol keep a stable order. Sorting uses plain codepoint comparison, so the order does not shift with the runtime locale.
An inactive market stays in the list carrying `isActive: false` rather than disappearing — whether it was delisted while the subscription was live or arrived inactive in the snapshot — so a consumer can tell "no longer tradable" from "never seen" and still resolve the metadata of positions, orders and history on it. Filter on `isActive` for the tradable set. A market absent from a snapshot is a different case — it is no longer listed at all, so it leaves the list with that snapshot.
The same entries keyed by trading pair UUID, for direct lookup.
Whether a subscription to the instrument channel is currently registered. This is **not** a connection indicator: the client accepts subscriptions while the socket is down and sends them on the next open, so `true` means "registered", not "receiving".
A rejected subscription request. The core helper validates its arguments synchronously — passing an empty `tradingPairId` is the reachable case — so treat this as a bad-usage signal.
It is deliberately **not** a transport signal: the client registers handlers regardless of socket state and logs malformed-frame failures internally, so connection loss and unparseable frames never appear here. Drive connection UI from the client's own status and resync callbacks instead.
Clear the current error state.
Each entry is an `InstrumentEventData` from `@0xmonaco/types`: `change`, `tradingPairId`, `symbol`, `isActive`, `tickSize`, `quantityStepSize`, `minOrderSize`, `maxOrderSize`, `marketType`, `category`, `changedFields`, `updatedAt`, plus the perp-only `maxLeverage`, `initialMarginRate`, and `maintenanceMarginRate` (omitted on spot markets). See the [channel reference](/reference/websockets#instrument) for the wire shape.
What makes the reconnect self-repairing is that the core helper delivers the snapshot as its own array rather than as per-market events, so the hook can tell where the baseline ends and drop entries missing from it. If a fresh snapshot is unavailable, the REST reconciliation above remains application-owned; reconnect does not itself fetch those endpoints.
## Example
```tsx theme={null}
import { useInstruments } from "@0xmonaco/react";
function MarketList() {
const { instruments, error } = useInstruments();
if (error) return Error: {error.message};
const tradable = instruments.filter((instrument) => instrument.isActive);
return (
Markets
{tradable.map((instrument) => (
-
{instrument.symbol} — tick {instrument.tickSize}, step {instrument.quantityStepSize}
{instrument.maxLeverage ? `, up to ${instrument.maxLeverage}x` : ""}
))}
);
}
```
### Following one market
```tsx theme={null}
function TickSize({ tradingPairId }: { tradingPairId: string }) {
const { instrumentsByPair } = useInstruments(tradingPairId);
const instrument = instrumentsByPair[tradingPairId];
if (!instrument) return null;
return Tick size: {instrument.tickSize};
}
```
# useMarket
Source: https://docs.0xmonaco.com/sdk/react/hooks/use-market
Query trading pairs and market data
```tsx theme={null}
import { useMarket } from "@0xmonaco/react";
const {
getPaginatedTradingPairs,
getTradingPairBySymbol,
getCandlesticks,
getMarketMetadata,
// Perp market helpers
getPerpMarketConfig,
getPerpMarketSummary,
getMarkPrice,
getIndexPrice,
getFundingState,
listFundingHistory,
getOpenInterest,
} = useMarket();
```
## Trading Pairs
Fetch paginated list of trading pairs.
**Returns:** `{ tradingPairs: TradingPair[], page, pageSize, total, totalPages }`
Get a specific trading pair by symbol. Resolves `undefined` on a miss (never throws), so guard the result before dereferencing. The hook forwards only `symbol`; to disambiguate a symbol listed in both SPOT and MARGIN, call `sdk.market.getTradingPairBySymbol(symbol, marketType)` on the core SDK directly.
## Market Data
Fetch OHLCV candlestick data for a trading pair.
**Parameters:**
* `tradingPairId`: string - Trading pair UUID (use `getTradingPairBySymbol` to get this)
* `interval`: `"1m" | "5m" | "15m" | "1h" | "4h" | "1d"`
* `params?`: Optional query parameters
* `startTime?`: number - Unix timestamp in ms
* `endTime?`: number - Unix timestamp in ms
* `limit?`: number - Max candlesticks (default: 350, max: 500)
* `priceType?`: `"trade" | "mark"` - price series to chart. `"trade"` (default) is the trade-derived OHLCV series; `"mark"` is the close-only mark-price series for margin pairs (each candle's open/high/low/close are the minute's closing mark, volume `0`). `"mark"` on a spot pair returns a `400`
**Example:**
```tsx theme={null}
const { getTradingPairBySymbol, getCandlesticks } = useMarket();
const pair = await getTradingPairBySymbol('BTC/USDC');
const candles = await getCandlesticks(pair.id, '1h', { limit: 100 });
```
Fetch market metadata for a trading pair including current price, volume, and statistics.
**Parameters:**
* `pairId`: string - Trading pair UUID
**Returns:** `MarketMetadata` object with market statistics
**Important:** All market metadata fields are nullable. Markets without trade history will return `null` for price and volume fields.
**Fields** (all nullable):
* `lastPrice: string | null` - Most recent trade price (null if no trades)
* `lastPriceTimestamp: number | null` - Timestamp of last trade
* `high24h: string | null` - 24-hour high price (null if insufficient data)
* `low24h: string | null` - 24-hour low price (null if insufficient data)
* `volume24h: string | null` - 24-hour trading volume in base-asset units (null if insufficient data)
* `quoteVolume24h?: string | null` - 24-hour trading volume in quote-token (USDC) units
* `priceChange24h: string | null` - 24-hour price change
* `priceChangePercent24h: string | null` - 24-hour price change percentage
* `marketInitializationTimestamp: number | null` - When market was initialized
**Example with null checks:**
```typescript theme={null}
const metadata = await getMarketMetadata(pair.id);
// Always check for null before using
if (metadata.lastPrice !== null) {
console.log(`Current price: ${metadata.lastPrice}`);
} else {
console.log('No trades yet for this market');
}
// Use nullish coalescing for defaults
const displayPrice = metadata.lastPrice ?? 'N/A';
const displayVolume = metadata.volume24h ?? '0';
```
## Perp Market Data
For margin concepts, see [Margin](/trading-mechanics/perps). For full type shapes, see the [TypeScript Market reference](/sdk/typescript/market).
Static perp config: `minLeverage`, `maxLeverage`, base initial/maintenance margin ratios, `fundingIntervalSeconds`, `riskTiers`, `liquidationFeeBps`.
Aggregate snapshot: `markPrice`, `indexPrice`, 24h stats (base-asset `volume24h` and quote-token `quoteVolume24h`), `openInterest` (base-asset, alias of `openInterestBase`), `openInterestBase`, `openInterestNotional` (base × mark, in quote units), `currentFundingRate`, `nextFundingTime`.
Live mark price (oracle reference). Returns `{ markPrice, oracleProvider, regime, updatedAt, ... }`.
Live index price with provider components. Returns `{ indexPrice, components: IndexComponent[], updatedAt }`.
Current and estimated next funding rate plus settlement time.
Settled funding history for one pair. Cursor-paginated: `pageSize` 1 to 1000 (default 50), `pageToken`, `orderBy` (`ASC`/`DESC`, default `DESC`), optional `startTime`/`endTime`. There is no `page`: the SDK rejects one with a `ValidationError` before sending. Follow `nextPageToken` until it comes back `""`.
Current open interest in base-asset units and notional.
**Example — funding countdown:**
```tsx theme={null}
import { useMarket } from "@0xmonaco/react";
import { useEffect, useState } from "react";
function FundingCountdown({ tradingPairId }: { tradingPairId: string }) {
const { getFundingState } = useMarket();
const [next, setNext] = useState(null);
const [rate, setRate] = useState(null);
useEffect(() => {
let cancelled = false;
const tick = async () => {
const state = await getFundingState(tradingPairId);
if (cancelled) return;
setNext(state.nextFundingTime);
setRate(state.currentFundingRate);
};
tick();
const id = setInterval(tick, 15000);
return () => { cancelled = true; clearInterval(id); };
}, [tradingPairId, getFundingState]);
if (!next) return null;
const msUntil = new Date(next).getTime() - Date.now();
return (
Funding in {Math.max(0, Math.floor(msUntil / 1000))}s — rate {rate}
);
}
```
# useMonacoSDK
Source: https://docs.0xmonaco.com/sdk/react/hooks/use-monaco
Access SDK instance and global state
```tsx theme={null}
import { useMonacoSDK } from "@0xmonaco/react";
const {
sdk,
clientId,
error,
clearError,
setError
} = useMonacoSDK();
```
Monaco SDK instance (null while initializing)
Application client ID from provider
SDK initialization error if any
Clear the current error
Set an error state
# useOHLCV
Source: https://docs.0xmonaco.com/sdk/react/hooks/use-ohlcv
Subscribe to real-time candlestick data
**New in v0.5.5** - Real-time candlestick data with WebSocket updates
Hook for subscribing to real-time OHLCV (Open, High, Low, Close, Volume) candlestick data via WebSocket.
## Usage
```tsx theme={null}
import { useOHLCV } from "@0xmonaco/react";
function CandlestickChart() {
const { candlesticks, loading, subscribed, error, clearError } = useOHLCV({
tradingPairId: "123e4567-e89b-12d3-a456-426614174000",
tradingMode: "SPOT",
interval: "1m",
initialFetchParams: { limit: 100 }
});
if (loading) return Loading candlesticks...
;
if (error) return Error: {error.message}
;
return (
OHLCV handler: {subscribed ? 'Registered' : 'Not registered'}
Candlesticks: {candlesticks.length}
{candlesticks.map((candle, i) => (
O: {candle.o} H: {candle.h} L: {candle.l} C: {candle.c} V: {candle.v}
))}
);
}
```
## Parameters
UUID of the trading pair. Get from `sdk.market.getTradingPairBySymbol()`.
Trading mode for OHLCV data. Pass `"MARGIN"` for a perp market's candles.
Candlestick time interval.
Optional parameters for initial data fetch:
* `limit` - Number of candlesticks to fetch (max 500)
* `startTime` - Start timestamp for range
* `endTime` - End timestamp for range
## Return Values
Array of candlestick data, sorted by time (oldest first). Updates in real-time via WebSocket.
`true` while fetching initial candlestick data from REST API, `false` once loaded.
`true` after the OHLCV channel handler is registered, `false` otherwise. This does not report socket connection or channel delivery state.
Error object if subscription or fetch fails, `null` otherwise.
Dismisses the current error. It does not retry the fetch or re-register the handler.
## Candlestick Data Structure
```typescript theme={null}
interface Candlestick {
T: number; // Candle start time (Unix timestamp in milliseconds)
t: number; // Candle end time (Unix timestamp in milliseconds)
o: string; // Open price
h: string; // High price
l: string; // Low price
c: string; // Close price
v: string; // Volume (base token)
s: string; // Symbol
i: string; // Interval
n: number; // Number of trades
}
```
## Features
### Hybrid Data Loading
1. **Initial REST Fetch**: Fetches historical candlesticks via REST API
2. **WebSocket Updates**: Subscribes to real-time updates after initial load
3. **Automatic Merging**: New candlesticks are automatically appended or update the last candle if timestamps match
### Stable Subscriptions
* **No data jitter**: Prevents state updates after component unmount
* **Proper cleanup**: WebSocket automatically unsubscribes when component unmounts
* **Race condition prevention**: Cancellation flag prevents stale updates
## Basic Example
```tsx theme={null}
import { useOHLCV } from "@0xmonaco/react";
function PriceChart() {
const { candlesticks, loading, subscribed } = useOHLCV({
tradingPairId: "pair-uuid-here",
tradingMode: "SPOT",
interval: "5m"
});
if (loading) return Loading chart...;
return (
OHLCV handler: {subscribed ? 'Registered' : 'Not registered'}
Latest Price: {candlesticks[candlesticks.length - 1]?.c}
);
}
```
## Different Time Intervals
```tsx theme={null}
function MultiTimeframeChart() {
const oneMinute = useOHLCV({
tradingPairId: "pair-uuid",
tradingMode: "SPOT",
interval: "1m"
});
const oneHour = useOHLCV({
tradingPairId: "pair-uuid",
tradingMode: "SPOT",
interval: "1h"
});
return (
1m Chart ({oneMinute.candlesticks.length} candles)
1h Chart ({oneHour.candlesticks.length} candles)
);
}
```
## Fetch Historical Data
```tsx theme={null}
function HistoricalChart() {
const endTime = Date.now();
const startTime = endTime - (24 * 60 * 60 * 1000); // 24 hours ago
const { candlesticks, loading } = useOHLCV({
tradingPairId: "pair-uuid",
tradingMode: "SPOT",
interval: "1h",
initialFetchParams: {
startTime,
endTime,
limit: 100
}
});
if (loading) return Loading...;
return Loaded {candlesticks.length} historical candles;
}
```
## Error Handling
```tsx theme={null}
function ChartWithErrorHandling() {
const { candlesticks, error, clearError, loading } = useOHLCV({
tradingPairId: "pair-uuid",
tradingMode: "SPOT",
interval: "1m"
});
if (error) {
return (
Failed to load chart: {error.message}
);
}
if (loading) return Loading...;
return {candlesticks.length} candles loaded;
}
```
## Display Latest Price
```tsx theme={null}
function LivePrice() {
const { candlesticks, subscribed } = useOHLCV({
tradingPairId: "pair-uuid",
tradingMode: "SPOT",
interval: "1m"
});
const latestCandle = candlesticks[candlesticks.length - 1];
if (!latestCandle) return No data;
return (
${latestCandle.c}
O: {latestCandle.o} H: {latestCandle.h} L: {latestCandle.l}
OHLCV handler: {subscribed ? 'Registered' : 'Not registered'}
);
}
```
## Calculate Price Change
```tsx theme={null}
function PriceChange() {
const { candlesticks } = useOHLCV({
tradingPairId: "pair-uuid",
tradingMode: "SPOT",
interval: "1d"
});
const [previousCandle, currentCandle] = candlesticks.slice(-2);
if (!previousCandle || !currentCandle) return Not enough data;
const current = parseFloat(currentCandle.c);
const previous = parseFloat(previousCandle.c);
const change = ((current - previous) / previous) * 100;
return (
24h Change: {change.toFixed(2)}%
= 0 ? 'green' : 'red' }}>
${current.toFixed(2)}
);
}
```
## Integration with Chart Libraries
```tsx theme={null}
import { useOHLCV } from "@0xmonaco/react";
import { Chart } from "react-chartjs-2";
function TradingViewChart() {
const { candlesticks, loading } = useOHLCV({
tradingPairId: "pair-uuid",
tradingMode: "SPOT",
interval: "1h",
initialFetchParams: { limit: 100 }
});
if (loading) return Loading chart...;
const chartData = {
labels: candlesticks.map(c => new Date(c.T).toLocaleTimeString()),
datasets: [{
label: 'Price',
data: candlesticks.map(c => parseFloat(c.c)),
borderColor: 'rgb(75, 192, 192)',
tension: 0.1
}]
};
return ;
}
```
## Notes
* The hook automatically manages WebSocket subscriptions and cleanup
* Candlesticks are updated in real-time when the WebSocket receives new events
* If the last candlestick has the same timestamp as an incoming update, it's replaced; otherwise, a new candle is appended
* Component unmounting automatically unsubscribes from the WebSocket channel
## See Also
* [WebSocket API](/websocket) - Low-level OHLCV WebSocket channel
* [TypeScript SDK](/sdk/typescript/websockets) - Core SDK WebSocket methods
* [useMarket](/sdk/react/hooks/use-market) - Get trading pair metadata
# useOrderbook
Source: https://docs.0xmonaco.com/sdk/react/hooks/use-orderbook
Real-time orderbook data via WebSocket
```tsx theme={null}
import { useOrderbook } from "@0xmonaco/react";
const {
orderbook,
subscribed,
error,
clearError
} = useOrderbook(tradingPairId, tradingMode, magnitude, quotationMode, depth?);
```
## Parameters
Trading pair UUID. Get this from `sdk.market.getTradingPairBySymbol('BTC/USDC')`.
Trading mode for the orderbook subscription. Use `'SPOT'` for spot markets and `'MARGIN'` for perp markets.
Price level grouping magnitude. Valid values: `0.0001`, `0.001`, `0.01`, `0.1`, `1`, `10`, `100`, `1000`, `10000`
Quantity denomination. `BASE` shows quantities in base token, `QUOTE` multiplies by price.
Number of price levels to fetch (max: 100)
## Return Values
Current orderbook snapshot with bids, asks, best prices, and volumes
**Properties:**
* `tradingPairId`: Trading pair ID (UUID)
* `tradingMode`: "SPOT" | "MARGIN"
* `bids`: Array of `{ price, quantity, orderCount }`
* `asks`: Array of `{ price, quantity, orderCount }`
* `bestBid` (optional): Best bid price
* `bestAsk` (optional): Best ask price
* `bidVolume` (optional): Total bid volume
* `askVolume` (optional): Total ask volume
* `priceChange`: Price change info (optional) - `{ side, oldPrice?, newPrice?, levelRemoved, levelAdded }`
* `baseDecimals`: Base token decimals
* `quoteDecimals`: Quote token decimals
* `timestamp`: Snapshot timestamp
* `sequence`: Sequence number for ordering
Whether currently subscribed to real-time updates
Connection or subscription error if any
Clear the current error
## Example
```tsx theme={null}
import { useOrderbook, useMarket } from "@0xmonaco/react";
import { useEffect, useState } from "react";
function OrderbookDisplay() {
const { getTradingPairBySymbol } = useMarket();
const [tradingPairId, setTradingPairId] = useState(null);
useEffect(() => {
getTradingPairBySymbol('BTC/USDC').then(pair => {
setTradingPairId(pair.id);
});
}, []);
const { orderbook, subscribed, error } = useOrderbook(
tradingPairId ?? '',
'SPOT',
1, // Group by $1 price levels
'BASE', // Show quantities in base token
20 // 20 levels each side
);
if (!tradingPairId) return Loading pair...;
if (error) return Error: {error.message};
if (!orderbook) return Loading orderbook...;
return (
Orderbook {subscribed ? '(live)' : '(connecting...)'}
Spread: ${(Number(orderbook.bestAsk) - Number(orderbook.bestBid)).toFixed(2)}
Bids
{orderbook.bids.slice(0, 10).map((level, i) => (
{level.quantity} @ ${level.price}
))}
Asks
{orderbook.asks.slice(0, 10).map((level, i) => (
{level.quantity} @ ${level.price}
))}
);
}
```
## How It Works
1. **Initial Load**: Fetches orderbook snapshot via REST API
2. **Real-time Updates**: Subscribes to WebSocket for live updates
3. **Auto-cleanup**: Unsubscribes when component unmounts or params change
The hook combines REST and WebSocket for the best experience - instant data on load, real-time updates after.
# usePitpass hooks
Source: https://docs.0xmonaco.com/sdk/react/hooks/use-pitpass
React hooks for TraderCodes referral codes and rewards
The TraderCodes hooks wrap the `sdk.pitpass.*` methods of `@0xmonaco/core`. Each is a
thin `useCallback` wrapper that resolves the client from the `MonacoProvider`
context, so the request, validation, and casing behaviour are owned by the core
client. Import each hook directly from `@0xmonaco/react`.
## useMyTraderCode
```tsx theme={null}
import { useMyTraderCode } from "@0xmonaco/react";
const { getMyTraderCode } = useMyTraderCode();
```
Read the authenticated caller's TraderCode identity. Requires authentication.
**Returns:**
* `code`: string - The permanent wallet-derived TraderCode (normalized wallet address)
* `createdAt`: string - RFC 3339 timestamp when the code row was first derived
* `customCode`: string - The custom vanity handle exactly as entered, or `""` when the wallet-derived default is kept
* `isCustom`: boolean - True when a custom handle is set
* `display`: string - The single human-facing string to render (custom handle when set, otherwise the `Monaco - ` default form)
## useSetTraderCode
```tsx theme={null}
import { useSetTraderCode } from "@0xmonaco/react";
const { setTraderCode } = useSetTraderCode();
```
Claim or edit the caller's custom (vanity) TraderCode handle. Requires authentication.
**Parameters:**
* `code`: string - The custom handle. Validated client-side (letter-first `[A-Za-z0-9_]`, length 5–20) and sent exactly as entered; casing is preserved
**Returns:** the same `TraderCodeResponse` shape as `useMyTraderCode`, reflecting the new custom handle.
## useClearTraderCode
```tsx theme={null}
import { useClearTraderCode } from "@0xmonaco/react";
const { clearTraderCode } = useClearTraderCode();
```
Clear the caller's custom TraderCode handle, reverting to the wallet-derived
default. The freed handle enters a cooldown before it can be reclaimed.
Requires authentication.
**Returns:** the `TraderCodeResponse` shape, now with the wallet-derived default in effect.
## useTraderCodeAvailability
```tsx theme={null}
import { useTraderCodeAvailability } from "@0xmonaco/react";
const { checkTraderCodeAvailability } = useTraderCodeAvailability();
```
Public, unauthenticated advisory check of whether a custom handle is available
(as-you-type UX). Advisory only — the authoritative gate is the atomic claim
in `useSetTraderCode`, so a positive result is not a reservation.
**Parameters:**
* `code`: string - The candidate custom handle
**Returns:**
* `available`: boolean - True when the handle passes validation and is neither live nor cooling down
* `reason`: "taken" | "too\_short" | "too\_long" | "reserved" | "invalid\_char" | "looks\_like\_address" (optional) - Present only when unavailable
## useTraderCodeInfo
```tsx theme={null}
import { useTraderCodeInfo } from "@0xmonaco/react";
const { getTraderCodeInfo } = useTraderCodeInfo();
```
Public lookup of a TraderCode by wallet address, used at sign-up to validate a
referral code before applying it. No authentication required.
**Parameters:**
* `code`: string - The bare wallet address or the `Monaco - ` display form
**Returns:**
* `code`: string - The resolved TraderCode (normalized wallet address)
## useRewardsBalance
```tsx theme={null}
import { useRewardsBalance } from "@0xmonaco/react";
const { getRewardsBalance } = useRewardsBalance();
```
Read the authenticated caller's current TraderCodes rewards-bucket balances.
Requires authentication.
**Returns:**
* `balances`: `{ token: string; available: string }[]` - One entry per reward token with a non-zero available balance. `available` is in RAW atomic units of the token
## useTransferRewards
```tsx theme={null}
import { useTransferRewards } from "@0xmonaco/react";
const { transferRewards } = useTransferRewards();
```
Transfer earned TraderCodes rewards into a tradeable balance. Parameters are
validated by the SDK core using Zod. Requires authentication.
**Parameters:**
* `token`: string - Reward token contract address (`0x`-prefixed) to transfer
* `amount`: string - Amount to transfer, in RAW atomic units of the token (positive integer string)
**Returns:**
* `rewardsBalance`: string - Your rewards-bucket balance after the transfer, RAW atomic units
* `tradingBalance`: string - Your trading balance for the token after the transfer, RAW atomic units
## useMyReferralPosition
```tsx theme={null}
import { useMyReferralPosition } from "@0xmonaco/react";
const { getMyReferralPosition } = useMyReferralPosition();
```
Read the authenticated caller's own referral position. Self-scoped — only ever returns your
own chain. Requires authentication.
**Returns:**
* `referrer`: string - Wallet address of your direct (L1) referrer, or `""` when you were not referred
* `upline`: `{ wallet, display, level }[]` - Your referral chain upward, nearest first (L1, L2, and so on), at most as many entries as TraderCodes rewards levels (never more than 10); empty when you were not referred
* `referrerDisplay`: string - The referrer's custom handle if claimed, otherwise the wallet address
* `totalEarned`: string - All-time rewards you have earned, RAW atomic units
* `rewards`: `{ source, sourceDisplay, level, amount, token, rateApplied, createdAt }[]` - Up to your 100 most recent reward rows (`totalEarned` remains the complete aggregate); `level` is 1 for a direct referee's fills, 2 for their referee's, and so on (at most 10)
## useMyReferralDownline
```tsx theme={null}
import { useMyReferralDownline } from "@0xmonaco/react";
const { getMyReferralDownline } = useMyReferralDownline();
```
Read who you earn from and how much. Self-scoped, read-only. Requires authentication.
`earningsBySource` is paged. Called without `params`, it returns only the **first page** (up to 100 rows), not every row — follow `nextPageToken` until it is `""` to read the rest.
**Parameters:**
* `pageSize?`: number - `earningsBySource` rows per page, 1-500 (default 100)
* `pageToken?`: string - Opaque `nextPageToken` from the previous response; omit for the first page
**Returns:**
* `earningsBySource`: `{ source, sourceDisplay, level, totalEarned, token }[]` - One page of rows, one per `(source, level, token)` across every reward level, ordered by each source account's total, largest first, so the same trader can appear more than once (different level or token); `source` is not unique. A wallet that traded through several applications is merged within a page at its first part's position, so a merged row can sit below a smaller one, and it can appear again on a later page
* `directReferees`: `{ wallet, display, referredAt, totalEarned }[]` - Your direct (L1) referees, including non-traders (`"0"` earned); up to the 500 most recent effective referees from a bounded scan, so a very large history may be incomplete (the earnings views stay complete)
* `summary`: `{ totalEarned, byLevel, level1Earned, level2Earned, level3Earned }` - Earnings broken down by level, covering every level and source regardless of the page. `byLevel` is `{ level, earned }[]` (raw atomic units), ascending, one entry per level with earnings, including levels deeper than TraderCodes rewards today. `level1Earned`, `level2Earned`, and `level3Earned` are **deprecated** (levels 1-3 only) - use `byLevel`
* `nextPageToken`: string - Pass it back as `pageToken` for the next page; `""` when there are no more rows
**Errors:** a `ValidationError` before sending when `pageSize` is outside 1-500; `400` for a malformed `pageToken`; `401` when unauthenticated; `429` when the account's read budget is exhausted (retry after the interval in the error detail); `500` on a server error
## useMyReferralTree
```tsx theme={null}
import { useMyReferralTree } from "@0xmonaco/react";
const { getMyReferralTree } = useMyReferralTree();
```
Read one level of your referral tree: your L1 referees when `parent` is omitted, or the referees
of one of your own referees above the deepest rewarded level when it is set. Self-scoped, read-only. Requires authentication.
**Parameters:**
* `parent?`: string - Wallet whose direct referees to list; any wallet that is not you or one of your referees above the deepest rewarded level returns 404
* `pageSize?`: number - Nodes per page, 1-200 (default 50)
* `pageToken?`: string - `nextPageToken` from the previous page (same `parent`)
**Returns:**
* `parent`: string - The wallet whose referees are listed
* `parentLevel`: number - `0` for you, `1`-`9` for an expanded referee
* `nodes`: `{ wallet, display, level, parent, referredAt, refereeCount, totalEarned }[]` - Newest first; `level` is the depth below you (1-10); expand a node when `refereeCount > 0` (always 0 at the deepest rewarded level); `totalEarned` is what you earned from that wallet's own trades
* `nextPageToken`: string - Cursor for the next page, `""` on the last page
# usePositions
Source: https://docs.0xmonaco.com/sdk/react/hooks/use-positions
React hook for managing perp positions
```tsx theme={null}
import { usePositions } from "@0xmonaco/react";
const {
listPositions,
getPosition,
getPositionRisk,
closePosition,
batchCloseAllPositions,
attachPositionTpSl,
listPositionHistory,
} = usePositions();
```
The hook returns the same surface as [`sdk.positions`](/sdk/typescript/positions). Each method is the same async function, ready to call from components or effects. There's no state, subscriptions, or auto-refresh built in — wrap calls in your own `useEffect` / SWR / React Query as suits your app.
For end-to-end flow and UI patterns, see [Positions](/developers/positions).
***
## Read
Paginated list of positions.
**Parameters:**
* `params?`:
* `marginAccountId?`: string
* `tradingPairId?`: string
* `status?`: `PositionStatusFilter` — `"OPEN" | "CLOSED" | "LIQUIDATING" | "LIQUIDATED"`
* `pageSize?`: number — 1 to 1000, defaults to 20
* `pageToken?`: string — pagination cursor: omitted or `""` starts the walk; a previous `nextPageToken` resumes it. There is no `page`: the SDK rejects one with a `ValidationError` before sending, and a raw REST request carrying it gets `400`
**Returns:** `{ positions: Position[]; total: number; pageSize: number; nextPageToken: string; totalCapped: boolean }`. Follow `nextPageToken` until it comes back `""`; each page over-fetches one row, so an empty token is definitive.
**Example — positions panel:**
```tsx theme={null}
function PositionsPanel({ marginAccountId }: { marginAccountId: string }) {
const { listPositions } = usePositions();
const [positions, setPositions] = useState([]);
useEffect(() => {
let cancelled = false;
(async () => {
const result = await listPositions({
marginAccountId: marginAccountId,
status: "OPEN",
});
if (!cancelled) setPositions(result.positions);
})();
return () => { cancelled = true; };
}, [marginAccountId, listPositions]);
return (
{positions.map((p) => (
-
{p.side} {p.size} @ {p.entryPrice} — PnL {p.unrealizedPnl}
))}
);
}
```
Full snapshot of a single position. Includes side, size, entry/mark/liquidation prices, isolated margin, leverage, and PnL. See [`Position` type](/sdk/typescript/positions).
Volatile risk fields (mark price, liquidation price, margin ratio, unrealized PnL). Cheaper than `getPosition` for live displays.
**Example — live liquidation distance:**
```tsx theme={null}
function LiquidationBar({ positionId }: { positionId: string }) {
const { getPositionRisk } = usePositions();
const [distance, setDistance] = useState(null);
useEffect(() => {
const tick = async () => {
const risk = await getPositionRisk(positionId);
const mark = parseFloat(risk.markPrice);
const liq = parseFloat(risk.liquidationPrice);
setDistance(mark > 0 ? (mark - liq) / mark : null);
};
tick();
const id = setInterval(tick, 2000);
return () => clearInterval(id);
}, [positionId, getPositionRisk]);
if (distance == null) return null;
return Liquidation distance: {(distance * 100).toFixed(2)}%;
}
```
Lifecycle events for one position or all positions in scope.
**Parameters:**
* `params?`:
* `positionId?`: string
* `marginAccountId?`: string
* `tradingPairId?`: string
* `reductionOnly?`: boolean — when `true`, returns only reductions, closes and liquidations
* `pageSize?`: number — 1 to 1000, defaults to 20
* `pageToken?`: string — pagination cursor: omitted or `""` starts the walk; a previous `nextPageToken` resumes it. There is no `page`: the SDK rejects one with a `ValidationError` before sending, and a raw REST request carrying it gets `400`
**Returns:** `{ events: PositionHistoryEvent[]; pageSize: number; nextPageToken: string }`. This listing carries no `total`: the relation spans the archive, so it is not counted behind a filter.
***
## Modify
This hook has no margin-adjustment method. Adjust a position's margin through the margin-account SDK surface — every leg
moves collateral between a risk bucket and the parent margin account, never to
or from the spot wallet:
* `transferCollateralToRiskBucket` adds margin: by default it commits the amount to the open position's stored margin (leverage drops and stays down); pass `applyToPositionMargin: false` to fund the bucket with spendable free collateral instead. The pair-scoped `transferCollateralToMarginAccount` is deprecated and matches the default.
* `transferCollateralFromMarginAccount` (with `tradingPairId`) releases collateral back to the parent.
Attach take-profit and/or stop-loss conditional orders to the position. At least one leg is required. Pass `oco: true` to cancel the sibling once one leg triggers (requires both legs).
**Parameters:**
* `positionId`: string
* `request`:
* `takeProfit?`: `TpSlLeg`
* `stopLoss?`: `TpSlLeg`
* `trailingStop?`: `TrailingStopLeg` — `{ trailBps, activationPrice?, closePosition?, quantity?, slippageToleranceBps?, expiresAt? }`; one per position, never part of `oco`. See [Trailing Stop](/developers/trade#trailing-stop)
* `oco?`: boolean — cancel the sibling leg after one triggers; requires both fixed legs
**`TpSlLeg`:** `{ triggerPrice, orderType: "MARKET" | "LIMIT", limitPrice?, closePosition?, quantity?, timeInForce?, slippageToleranceBps?, expiresAt? }`. Set `closePosition: true` to resolve the full live position size on trigger (omit `quantity`). See [Order Management](/developers/trade#conditional-perps-orders).
**Example — TP/SL modal submit:**
```tsx theme={null}
const { attachPositionTpSl } = usePositions();
async function onSubmit(values: { tp: string; sl: string }) {
await attachPositionTpSl(positionId, {
takeProfit: { triggerPrice: values.tp, orderType: "MARKET", closePosition: true },
stopLoss: { triggerPrice: values.sl, orderType: "MARKET", closePosition: true },
oco: true,
});
}
```
***
## Close
Submit a closing order. Position transitions to `CLOSED` only after the closing order fills.
**Parameters:**
* `positionId`: string
* `request`:
* `closeType`: `"MARKET" | "LIMIT" | "IOC"`
* `limitPrice?`: string — required for `LIMIT` and `IOC`, forbidden only for `MARKET`
* `quantity?`: string — defaults to full position size
* `slippageToleranceBps?`: number — for `MARKET` / `IOC`
**Example — partial market close:**
```tsx theme={null}
const { closePosition } = usePositions();
await closePosition(positionId, {
closeType: "MARKET",
quantity: "0.5",
});
```
Submit a MARKET reduce-only close for every open position owned by the caller — a one-call "panic close". Best-effort and partial: each position closes independently, and per-position failures are reported in `results` instead of aborting the batch. See [`BatchCloseAllResponse` type](/sdk/typescript/positions).
**Parameters:**
* `request?`:
* `tradingPairId?`: string — limit the close to one trading pair; if omitted, all open positions are closed
* `slippageToleranceBps?`: number — applied to each MARKET close
**Example — close-all button:**
```tsx theme={null}
const { batchCloseAllPositions } = usePositions();
async function onCloseAll() {
const { totalClosed, totalFailed } = await batchCloseAllPositions();
if (totalFailed > 0) {
toast.warn(`${totalClosed} closing, ${totalFailed} failed`);
}
}
```
***
## Subscribing to lifecycle events
`usePositions` doesn't ship a built-in subscription. For live updates of conditional-order lifecycle (TP/SL state changes), use `sdk.ws.conditionalOrders` — see the [WebSocket reference](/sdk/typescript/websockets).
For order-fill confirmations after a `closePosition` or attached-TP/SL trigger, subscribe to `sdk.ws.userOrders`.
# useProfile
Source: https://docs.0xmonaco.com/sdk/react/hooks/use-profile
User profile and account information
```tsx theme={null}
import { useProfile } from "@0xmonaco/react";
const {
profile,
balances,
movements,
orders,
loading,
fetchProfile,
getPaginatedUserMovements,
getUserBalances,
getUserBalanceByAssetId,
getPaginatedOrders,
} = useProfile();
```
## State
Core profile metadata (id, address, username, account type, withdrawal status). Undefined until fetched. Does not include balances, movements, or orders.
User's token balances. Populated by `fetchProfile()` (first 20) or `getUserBalances()`. Empty array until fetched.
User's ledger movements. Populated by `fetchProfile()` (first cursor page, `pageSize` 20) or `getPaginatedUserMovements()`. Undefined until fetched.
User's orders. Populated by `fetchProfile()` (first cursor page, pageSize 20) or `getPaginatedOrders()`. Undefined until fetched.
True while `fetchProfile()` is in flight. Does not reflect individual fetch method calls.
## Actions
Fetches the core profile and the first page of balances, movements, and orders in parallel. Updates all state fields. Throws if the profile fetch fails; balance/movement/order fetch failures are silent (state remains unchanged for those fields).
Fetch a cursor page of token balances. Updates `balances` state.
**Parameters:**
* `pageSize` (optional): Number of items per page, 1 to 1000 (default: 20). A larger value is rejected, not clamped
* `pageToken` (optional): cursor — omit or send `""` for the first page, then pass the previous response's `nextPageToken`. The walk seeks on `(token, assetId)` over the set the server overlays with the matching engine's authoritative amounts, so a balance the engine adds mid-walk no longer appears only at the end of the last page
There is no `page`: the SDK rejects one with a `ValidationError` before sending, and a raw REST request carrying it gets `400`.
**Returns:** `balances` plus `nextPageToken` — pass it back as `pageToken` and keep going until it comes back empty.
Get the balance for a specific asset by its UUID. Does not update `balances` state.
Fetch paginated transaction history. Updates `movements` state.
**Parameters:**
* `pageSize` (optional): Number of items per page, 1 to 1000 (default: 20). A larger value is rejected, not clamped
* `pageToken` (optional): cursor — omit or send `""` for the first page, then pass the previous response's `nextPageToken`. The walk seeks on `(createdAt, id)`, both immutable, so a movement written mid-walk cannot make the walk repeat or drop a row
* `entryType` (optional): Filter by entry type (`LedgerEntryType`, upper-case)
* `transactionType` (optional): Filter by transaction type (`TransactionType`, upper-case)
* `assetId` (optional): Filter by asset UUID
* `orderBy` (optional): `"ASC"` or `"DESC"` (default `"DESC"`); a `pageToken` resumes only under the same `orderBy`
There is no `page`: the SDK rejects one with a `ValidationError` before sending, and a raw REST request carrying it gets `400`. The filters must match the request that minted the token.
**Returns:** `movements` plus `nextPageToken` — pass it back as `pageToken` and keep going until it comes back empty.
Fetch paginated orders. Updates `orders` state.
**Parameters:**
* `status`, `tradingPairId`, `tradingMode`, `marginAccountId` (optional): filters
* `pageSize` (optional): Items per page, up to 1000 (default: 10)
* `pageToken` (optional): cursor — omit or send `""` for the first page, then pass the previous response's `nextPageToken`. See [Order queries](/sdk/typescript/trades#order-queries)
# useTokenLifecycle
Source: https://docs.0xmonaco.com/sdk/react/hooks/use-token-lifecycle
Persist and auto-refresh the authenticated session
```tsx theme={null}
import { useTokenLifecycle } from "@0xmonaco/react";
const {
initializeTokens,
clearTokens,
refreshTokens,
isTokenExpired
} = useTokenLifecycle(sdk, config);
```
Manages the lifecycle of the authenticated session: persists the session across reloads and refreshes it before it expires. The session is the wallet-authorized ed25519 keypair returned by `login()`; there are no separate access or refresh tokens. Most apps don't use this hook directly; `MonacoProvider` wires it up and `useAuth` drives it. See [React SDK setup](/sdk/react/intro).
## Parameters
Monaco SDK instance from `useMonacoSDK()`
Session lifecycle options:
* `autoRefresh`: boolean (default: `true`) - automatically extend the session before it expires
* `persistTokens`: boolean (default: `true`) - persist the session in `localStorage` so it survives reloads
* `refreshBufferSeconds`: number (default: `300`) - how long before expiry to trigger a refresh
* `onRefreshError`: `(error: Error) => void` - called when a refresh fails
* `onRefreshSuccess`: `(authState: AuthState) => void` - called when a refresh succeeds
## Return Values
Start managing a session. Call after a successful `login()`; persists the session (if enabled) and schedules auto-refresh.
Stop managing the session, cancel scheduled refreshes, and remove the persisted session. Call on logout.
Extend the session's expiry now (via `sdk.refreshAuth()`). Returns the updated `AuthState`, or `null` if there is no active session or the refresh fails.
Whether the session is expired or within the refresh buffer of expiry.
## Example
```tsx theme={null}
import { useMonacoSDK, useTokenLifecycle } from "@0xmonaco/react";
function SessionManager() {
const { sdk } = useMonacoSDK();
const {
initializeTokens,
clearTokens,
refreshTokens,
isTokenExpired
} = useTokenLifecycle(sdk, {
autoRefresh: true,
persistTokens: true,
refreshBufferSeconds: 300, // refresh 5 minutes before expiry
onRefreshError: (error) => {
console.error('Session refresh failed:', error);
// Handle logout or re-authentication
},
onRefreshSuccess: (authState) => {
console.log('Session extended to', authState.expiresAt);
},
});
// After a successful login
const handleLoginSuccess = (authState) => {
initializeTokens(authState);
};
// On logout
const handleLogout = () => {
clearTokens();
};
// Manual refresh if needed
const handleManualRefresh = async () => {
const newAuthState = await refreshTokens();
if (!newAuthState) {
console.log('Refresh failed, need to re-authenticate');
}
};
return (
);
}
```
## How It Works
1. **Auto Refresh**: when enabled, schedules a session refresh `refreshBufferSeconds` before expiry.
2. **Persistence**: saves the session to `localStorage` so it can be restored after a reload.
3. **Deduplication**: collapses concurrent refresh calls into a single request.
4. **Cleanup**: clears scheduled timers on unmount.
When `persistTokens` is enabled, the persisted session includes the ed25519 **session private key**. This credential signs every request. `localStorage` is readable by any script on your origin, so harden against XSS (e.g. a strict Content Security Policy) and always `clearTokens()` on logout. Disable `persistTokens` to keep the session in memory only.
This is an advanced hook. For most apps, `MonacoProvider` + `useAuth` manage the session automatically.
# useTrade
Source: https://docs.0xmonaco.com/sdk/react/hooks/use-trade
Place and manage orders, including perp orders and conditional TP/SL
```tsx theme={null}
import { usePositions, useTrade } from "@0xmonaco/react";
const {
placeLimitOrder,
placeMarketOrder,
cancelOrder,
batchCancel,
batchCancelAll,
replaceOrder,
batchCreate,
batchReplace,
cancelConditionalOrder,
listConditionalOrders,
getPaginatedOrders,
getOrder,
} = useTrade();
```
The hook returns the same surface as [`sdk.trading`](/sdk/typescript/trades).
## Place Orders
Place a limit order. Spot by default; opt into perp with `tradingMode: "MARGIN"` and `leverage`.
**`options`:**
* `tradingMode?`: `"SPOT" | "MARGIN"`
* `useMasterBalance?`: boolean
* `expirationDate?`: string — ISO 8601; pair with `GTC` for an order that expires at a set time
* `timeInForce?`: `"GTC" | "IOC" | "FOK"` (`GTD` is declared in the protos but never implemented — the server rejects it)
* `postOnly?`: boolean — maker-only; the order is rejected instead of matching if it would cross the book. `GTC` only — rejected on `MARKET` orders and with `IOC`/`FOK`. See [Post-Only Orders](/developers/trade#post-only-orders)
* `selfTradePreventionMode?`: `"CANCEL_MAKER" | "CANCEL_TAKER" | "CANCEL_BOTH" | "SKIP"` — how the engine resolves this order meeting one of your own resting orders (your whole wallet family). Overrides the wallet default; omit to inherit that, or the platform default `CANCEL_MAKER`. See [Self-Trade Prevention](/developers/trade#self-trade-prevention)
* `leverage?`: string — decimal string; required for normal margin orders, omit only for reduce-only
* `reduceOnly?`: boolean — order can only shrink existing exposure
* `marginAccountId?`: string — optional legacy override; omit for normal perp orders
* `positionSide?`: `"LONG" | "SHORT" | "NONE"` — deprecated. `side` is the authoritative direction; if supplied, `positionSide` must agree (`BUY`↔`LONG`, `SELL`↔`SHORT`)
* `clientOrderId?`: string — correlation handle echoed on order reads and `orders` WebSocket events (≤64 chars from `A-Za-z0-9._:-`); not an idempotency key. Restate it on `replaceOrder` — a replacement inherits nothing. See [Client Order IDs](/developers/trade#client-order-ids)
* `idempotencyKey?`: string — durable submission key. The SDK generates one for this call when omitted; retain and reuse an explicit key with the same payload to recover across calls or restarts within 24 hours. See [Safe Retries](/developers/trade#safe-retries-with-idempotency-keys)
Perps are one-way — `side` sets the direction. Attach TP/SL at entry with `takeProfit`/`stopLoss` (margin entry orders only; legs activate when the order fills), or attach to the open position with [`usePositions().attachPositionTpSl`](/sdk/react/hooks/use-positions). A `trailingStop` leg attaches the same way, arms only when the order fully fills, and returns `trailingStopOrderId` only when the trailing stop materializes (a full fill on arrival or a resting remainder; a partial `IOC` / `MARKET` fill whose remainder is cancelled creates none) — see [Trailing Stop](/developers/trade#trailing-stop).
**Perp example:**
```tsx theme={null}
const { placeLimitOrder } = useTrade();
const { attachPositionTpSl } = usePositions();
// Open a 10x long with TP/SL attached at entry — the legs activate when
// the order fills and cancel if it is cancelled or expires unfilled.
await placeLimitOrder(tradingPairId, "BUY", "1.5", "2000", {
tradingMode: "MARGIN",
leverage: "10",
takeProfit: { triggerPrice: "2200", orderType: "MARKET" },
stopLoss: { triggerPrice: "1900", orderType: "LIMIT", limitPrice: "1895" },
});
// Or attach / adjust TP/SL after the position opens
await attachPositionTpSl(positionId, {
takeProfit: { triggerPrice: "2200", orderType: "MARKET" },
stopLoss: { triggerPrice: "1900", orderType: "LIMIT", limitPrice: "1895" },
});
```
Place a market order. Market orders execute inside a mandatory server-side price band, 1,000 bps by default (see [Market-order price protection](/trading-mechanics/spot#market-order-price-protection); a perp market can set its own) and can partially fill with the remainder cancelled, or be rejected when no liquidity sits inside the band.
**`options`:**
* `tradingMode?`: `"SPOT" | "MARGIN"`
* `slippageTolerance?`: number — tightens the market's price band (e.g. `0.01` for 1%); cannot widen it
* `leverage?`: string — required for normal margin orders, omit only for reduce-only
* `reduceOnly?`: boolean
* `marginAccountId?`: string — optional legacy override; omit for normal perp orders
* `positionSide?`: `"LONG" | "SHORT" | "NONE"` — deprecated; `side` is authoritative
* `clientOrderId?`: string — correlation handle echoed on order reads and `orders` WebSocket events (≤64 chars from `A-Za-z0-9._:-`); not an idempotency key. See [Client Order IDs](/developers/trade#client-order-ids)
* `selfTradePreventionMode?`: `"CANCEL_MAKER" | "CANCEL_TAKER" | "CANCEL_BOTH" | "SKIP"` — how the engine resolves this order meeting one of your own resting orders (your whole wallet family). Overrides the wallet default; omit to inherit that, or the platform default `CANCEL_MAKER`. See [Self-Trade Prevention](/developers/trade#self-trade-prevention)
* `idempotencyKey?`: string — durable submission key. The SDK generates one for this call when omitted; retain and reuse an explicit key with the same payload to recover across calls or restarts within 24 hours. See [Safe Retries](/developers/trade#safe-retries-with-idempotency-keys)
## Order Management
Cancel an existing order
Cancel specific orders by their IDs.
**Returns:**
* `totalRequested`: number
* `totalCancelled`: number
* `totalFailed`: number
* `results`: `BatchCancelResult[]` — each with `orderId`, optional `cancelledAt`, optional `error: { code, message }`
Use `totalFailed === 0` for an overall-success check.
Capped at **100 order IDs** per request by the API (enforced server-side, no client-side schema check); a longer list is rejected with `400 At most 100 orders per batch request`. `batchCancelAll` enumerates active orders server-side, with a separate cap of **20,000 matching active orders**.
**Example:**
```tsx theme={null}
const { batchCancel } = useTrade();
async function handleCancelSelected(orderIds: string[]) {
const result = await batchCancel(orderIds);
console.log(`Cancelled ${result.totalCancelled} of ${result.totalRequested}`);
result.results.forEach((r) => {
if (r.error) console.error(`Order ${r.orderId} failed: ${r.error.message}`);
});
}
```
Cancel all active orders, optionally scoped to one trading pair.
The API caps the request at **20,000 matching active orders**. Above that cap, it cancels nothing and returns REST `400`; narrow by trading pair or cancel explicit batches of at most 100 IDs. The cap check is all-or-nothing and runs before any cancellation.
Returns the same shape as `batchCancel`.
**Throws:**
* REST `400` when the pair UUID is invalid or more than 20,000 matching active orders would be cancelled
* REST `401` when authentication is missing or invalid
* REST `403` when a delegated session omits the required pair scope or lacks permission
* REST `500` for an internal matching-engine failure
* REST `503` for a transient matching-engine or transport failure
**Example:**
```tsx theme={null}
const { batchCancelAll } = useTrade();
async function handleCancelAll() {
const result = await batchCancelAll();
alert(`Cancelled ${result.totalCancelled} orders`);
}
```
Modify an existing order's price or quantity. `quantity` is the order's new **total** (a partially filled order rests `total - filled` and must exceed the filled amount; omit it to keep the total), and the replacement inherits the original's fill history — see [`replaceOrder`](/sdk/typescript/trades) for the full semantics. `newOrder` also accepts `postOnly`, `clientOrderId` and `selfTradePreventionMode` — a replacement is a new order and inherits none of them, so restate `clientOrderId` to keep the handle and `selfTradePreventionMode` to keep the override. `batchReplace` items take the same three. See [Client Order IDs](/developers/trade#client-order-ids).
## Batch Operations
Create multiple orders in a single request. Capped at **100 items** — a longer array is rejected client-side (`At most 100 orders per batch request`, exported as `MAX_BATCH_ORDER_ITEMS`) before the request is sent.
**`BatchCreateOrderParams`:**
* `tradingPairId`: string
* `orderType`: `"LIMIT" | "MARKET"`
* `side`: `"BUY" | "SELL"`
* `quantity`: string
* `price?`: string — required for LIMIT
* `timeInForce?`: `"GTC" | "IOC" | "FOK"` — LIMIT items only; the SDK rejects it on a MARKET item (market orders take no time-in-force and always execute IOC-style inside the price band)
* `postOnly?`: boolean — maker-only; the item is rejected instead of matching if it would cross the book, reported per-item as `error.code === "POST_ONLY_WOULD_CROSS"`. LIMIT items with `GTC` only. See [Post-Only Orders](/developers/trade#post-only-orders)
* `selfTradePreventionMode?`: `"CANCEL_MAKER" | "CANCEL_TAKER" | "CANCEL_BOTH" | "SKIP"` — per-item self-trade-prevention override. The value is validated client-side, so an unknown mode throws `ValidationError` before the request is sent; the per-item `error.code` `INVALID_SELF_TRADE_PREVENTION_MODE` is the raw REST/gRPC caller's shape. See [Self-Trade Prevention](/developers/trade#self-trade-prevention)
* `slippageTolerance?`: number — for MARKET
* `expirationDate?`: string — ISO 8601, custom expiry for GTC
* `useMasterBalance?`: boolean
* `clientOrderId?`: string — per-item correlation handle echoed on order reads and `orders` WebSocket events (≤64 chars from `A-Za-z0-9._:-`); a malformed value fails that item with `error.code === "INVALID_CLIENT_ORDER_ID"`. Not an idempotency key. See [Client Order IDs](/developers/trade#client-order-ids)
* `idempotencyKey?`: string — per-item durable submission key. The SDK generates one per item when omitted. Retain explicit per-item keys for recovery across calls; each key protects only its own item and does not make the batch atomic. See [Safe Retries](/developers/trade#safe-retries-with-idempotency-keys)
**Returns — `BatchCreateOrdersResponse`:**
* `totalRequested`: number
* `totalSucceeded`: number
* `totalFailed`: number
* `results`: `BatchCreateResult[]`
**`BatchCreateResult`:**
* `orderId`: string — empty if creation failed before ID assignment
* `matchResult?`: `MatchResult` — present if order was immediately matched
* `error?`: `BatchError` — `{ code, message }`. Treat absence of `error` as success.
**Example:**
```tsx theme={null}
import { useTrade } from "@0xmonaco/react";
function TradingPanel({ pairId }: { pairId: string }) {
const { batchCreate } = useTrade();
async function placeBracketOrders() {
const result = await batchCreate([
{ tradingPairId: pairId, orderType: "LIMIT", side: "BUY", quantity: "1.0", price: "2000.00" },
{ tradingPairId: pairId, orderType: "LIMIT", side: "BUY", quantity: "0.5", price: "1950.00" },
]);
result.results.forEach((r) => {
if (r.error) {
console.error(`Failed: ${r.error.code} — ${r.error.message}`);
} else {
console.log(`Order ${r.orderId} created`);
}
});
}
}
```
Replace multiple orders in a single request. Each replacement atomically cancels the original and creates a new one. Capped at **100 items** — a longer array is rejected client-side (`At most 100 orders per batch request`) before the request is sent.
**`BatchReplaceOrderParams`:**
* `orderId`: string
* `price?`: string
* `quantity?`: string — the order's new **total** (same rule as `replaceOrder`): a partially filled order rests `total - filled` and the total must exceed the filled amount; omit it to keep the order's total. Margin reduce-only orders follow the same rule, and the replacement inherits the original's fill history
* `useMasterBalance?`: boolean
* `clientOrderId?`: string — per-item correlation handle for the replacement order (a replacement does not carry over the original's `clientOrderId`, so restate it). A malformed handle fails **its item alone** (`error.code` `INVALID_CLIENT_ORDER_ID`, the same code batch-create uses) with its original untouched. See [Client Order IDs](/developers/trade#client-order-ids)
* `postOnly?`: boolean — maker-only intent for the replacement; never inherited from the original. See [Post-Only Orders](/developers/trade#post-only-orders)
* `selfTradePreventionMode?`: `"CANCEL_MAKER" | "CANCEL_TAKER" | "CANCEL_BOTH" | "SKIP"` — per-item self-trade-prevention override for the replacement, likewise never inherited; omit it to fall back to your wallet default. An unknown value is rejected client-side with `ValidationError` before sending; `error.code` `INVALID_SELF_TRADE_PREVENTION_MODE` is what a raw REST/gRPC caller gets on that item alone. See [Self-Trade Prevention](/developers/trade#self-trade-prevention)
Every item must still change something: `BatchReplaceOrderItemSchema` requires **at least one of `price` or `quantity`**, so an item carrying only `selfTradePreventionMode` (or only `postOnly`) throws `ValidationError` before the request is sent. Restate the price or quantity alongside the option.
**Returns — `BatchReplaceOrdersResponse`:**
* `totalRequested`, `totalSucceeded`, `totalFailed`: number
* `results`: `BatchReplaceResult[]`
**`BatchReplaceResult`:**
* `originalOrderId`: string
* `newOrderId?`: string
* `updatedFields?`: `{ price?, quantity? }`
* `matchResult?`: `MatchResult`
* `error?`: `BatchError` — `{ code, message }`. Absence of `error` indicates success.
**Example:**
```tsx theme={null}
const { batchReplace } = useTrade();
async function adjustPrices(orders: { id: string; newPrice: string }[]) {
const result = await batchReplace(
orders.map(({ id, newPrice }) => ({ orderId: id, price: newPrice })),
);
result.results.forEach((r) => {
if (r.error) {
console.error(`Failed: ${r.error.code} — ${r.error.message}`);
} else {
console.log(`New order ID: ${r.newOrderId} (replaced ${r.originalOrderId})`);
}
});
}
```
## Conditional Orders (Perp TP/SL)
Attach TP/SL and trailing stops at entry via `takeProfit`/`stopLoss`/`trailingStop` on `placeLimitOrder` / `placeMarketOrder`, or to an open position with [`usePositions().attachPositionTpSl`](/sdk/react/hooks/use-positions). The hook below covers listing and cancelling conditional orders. Concepts and lifecycle: [Order Management](/developers/trade#conditional-perps-orders).
Cancel an active conditional order.
List conditional orders for the authenticated user. Passes `params` straight to `sdk.trading.listConditionalOrders`, so cursor (`pageToken`) pagination is the default: follow each response's `nextPageToken` until it comes back empty.
**Parameters:**
* `marginAccountId?`, `tradingPairId?`, `state?`, `pageToken?`, `pageSize?` (up to `1000`)
## Order Queries
Fetch paginated orders
Get order details by ID
# useTradeFeed
Source: https://docs.0xmonaco.com/sdk/react/hooks/use-trade-feed
Real-time trade feed via WebSocket
```tsx theme={null}
import { useTradeFeed } from "@0xmonaco/react";
const {
trades,
subscribed,
fetchingInitialState,
error
} = useTradeFeed(tradingPairId);
```
## Parameters
Trading pair UUID. Get this from `sdk.market.getTradingPairBySymbol('ETH/USDC')`.
The trade feed maintains a **fixed limit of 50 trades**. This limit is not configurable and is enforced internally to optimize memory and performance.
## Return Values
Array of recent trades (newest first). Fixed at 50 trades maximum.
Whether currently subscribed to the trading pair. Will be false if SDK is not yet initialized.
Whether the initial trades are being fetched via REST API.
WebSocket error if any
## Example
```tsx theme={null}
import { useTradeFeed, useMarket } from "@0xmonaco/react";
import { useEffect, useState } from "react";
function TradeFeed() {
const { getTradingPairBySymbol } = useMarket();
const [tradingPairId, setTradingPairId] = useState(null);
useEffect(() => {
getTradingPairBySymbol('ETH/USDC').then(pair => {
setTradingPairId(pair.id);
});
}, []);
const { trades, subscribed, error } = useTradeFeed(tradingPairId ?? '');
if (!tradingPairId) return Loading...;
if (error) return Error: {error.message};
return (
Recent Trades {subscribed ? '(live)' : '(connecting...)'}
{trades.map(trade => (
-
{trade.data.makerSide} {trade.data.quantity} @ {trade.data.price}
))}
);
}
```
# useUserBalances
Source: https://docs.0xmonaco.com/sdk/react/hooks/use-user-balances
Real-time balance tracking and updates via WebSocket
**New in v0.5.6** - Real-time balance tracking with WebSocket updates
Subscribe to real-time updates for all your token balances. Automatically fetches initial balances via REST API and keeps them synchronized with WebSocket events.
```tsx theme={null}
import { useUserBalances } from "@0xmonaco/react";
const {
balances,
loading,
subscribed,
error,
clearError,
refresh
} = useUserBalances();
```
## Return Values
Array of user token balances, automatically updated in real-time. Merged from REST API initial data and WebSocket updates.
`true` while fetching initial balances from REST API, `false` once loaded
`true` when WebSocket subscription is active, `false` otherwise
Error object if something went wrong during fetch or subscription, `null` otherwise
Function to clear the current error state
Manually refresh balances from REST API (useful for force refresh after errors or manual updates)
## Features
### Hybrid Data Loading
1. **Initial Load**: reads every page of balances via REST (`getUserBalances()`), which is paginated — a single page would leave assets past the first unreachable.
2. **WebSocket Subscription**: subscribes to the `balances` channel after the initial load.
3. **Race Condition Prevention**: the subscription only opens once the REST data is in, so an event cannot be overwritten by the response it raced.
4. **Subscribe-time Snapshot**: the channel's [snapshot](/sdk/typescript/websockets#subscribe-time-snapshots) carries every balance, and the hook applies it to the list it already has — no second REST read.
### Applying the snapshot
The snapshot is the complete balance set, which makes it different from a stream of updates in two ways:
* Its totals and `version` are taken **verbatim** from the same durable spot row as REST.
* A held asset the snapshot **omits** is zeroed rather than left showing a figure that is no longer true. The row itself stays, so a portfolio renders "0 WSEI" instead of losing the line. A held row carrying a real `version` is the exception: a versioned zero is an explicit tombstone, so omission cannot be read as "this went to zero" and the row is retained until a newer one arrives.
This matters most on reconnect: the resubscribe brings a fresh snapshot, and balances that moved while the socket was down are corrected from it without a REST round trip.
### Real-time Updates
The hook listens to balance update events triggered by:
* **Deposits** - Tokens deposited to the vault
* **Withdrawals** - Tokens withdrawn from the vault
* **Order Locking** - Balance locked when placing orders
* **Order Unlocking** - Locked balance released when orders are cancelled or expire
* **Trades** - Balance changes from order fills and fee deductions
* **Margin Transfers** - Collateral moved into or out of a margin account
A versioned spot event replaces the whole spot row only when its `version` is
higher. Equal or lower versions are idempotent. A collateral transfer that
moves the spot wallet row is versioned like any other spot event. Margin
collateral notifications — a parent to risk-bucket allocation, a
margin-routed deposit — remain unversioned because they are not
authoritative `user_balances` rows; the hook applies those notifications
fail-open without changing the held spot version. Refresh margin-account state
separately. `updatedAt` is never used to order rows.
### Automatic Balance Updates
When a balance update event is received:
* If the token already exists in state, the balance is updated with the new values
* If a new token appears, the hook re-reads from REST — the wire payload carries no `assetId`, `decimals` or wrapped-native flag, so a new asset cannot be built from an event alone
* Balance updates include both normalized (human-readable) and raw (wei) values
That background re-read **merges** rather than replaces. It runs with the subscription live, so its response is already potentially stale by the time it lands; it therefore adds only genuinely new assets and leaves every held row's live values untouched. Wire state for an unknown asset is buffered until the read supplies its metadata, then replayed through the same version arbitration as an ordinary event or snapshot. The read is also coalesced — a burst of frames for one unknown asset costs a single read plus one trailing read, rather than one per frame — and it does not raise `loading`, which describes the initial load. The public `refresh()` re-reads the full set and applies the same version arbitration: a fetched row supersedes the held one unless the held row carries a newer `version`, and a held versioned row the fetch omits is retained rather than dropped. So it is a version-aware refresh, not a blind overwrite — an in-flight REST read cannot roll a balance back to a state the stream has already moved past.
### Authentication Required
This hook requires an authenticated session. Log in with the [`useAuth`](/sdk/react/hooks/use-auth) hook (or `sdk.login(...)`) before using it.
## Basic Usage
```tsx theme={null}
import { useUserBalances } from "@0xmonaco/react";
function BalancesList() {
const { balances, loading, subscribed, error } = useUserBalances();
if (loading) {
return Loading balances...;
}
if (error) {
return Error: {error.message};
}
return (
WebSocket: {subscribed ? "Connected" : "Disconnected"}
Your Balances
{balances.map(balance => (
{balance.symbol || balance.token.slice(0, 8)}
Available: {balance.availableBalance}
Locked: {balance.lockedBalance}
Total: {balance.totalBalance}
))}
);
}
```
## With Refresh Button
```tsx theme={null}
function BalancesWithRefresh() {
const { balances, loading, refresh } = useUserBalances();
return (
Your Balances
{balances.map(balance => (
{balance.symbol || "Unknown Token"}
Available: {balance.availableBalance}
Locked: {balance.lockedBalance}
Total: {balance.totalBalance}
))}
);
}
```
## Filter Non-Zero Balances
```tsx theme={null}
function ActiveBalances() {
const { balances } = useUserBalances();
// Filter balances with non-zero total
const activeBalances = balances.filter(
balance => parseFloat(balance.totalBalance) > 0
);
return (
Active Balances ({activeBalances.length})
{activeBalances.map(balance => (
{balance.symbol}
{balance.totalBalance}
{parseFloat(balance.lockedBalance) > 0 && (
Locked: {balance.lockedBalance}
)}
))}
);
}
```
## Balance Summary with Charts
```tsx theme={null}
import { useUserBalances } from "@0xmonaco/react";
function BalanceSummary() {
const { balances, loading } = useUserBalances();
const totalTokens = balances.length;
const activeTokens = balances.filter(b => parseFloat(b.totalBalance) > 0).length;
const totalLocked = balances.reduce(
(sum, b) => sum + parseFloat(b.lockedBalance || "0"),
0
);
if (loading) {
return Loading balances...;
}
return (
Total Tokens
{totalTokens}
Active Tokens
{activeTokens}
Tokens with Locked Balance
{balances.filter(b => parseFloat(b.lockedBalance) > 0).length}
Balance Details
{balances.map(balance => (
{balance.symbol || "Unknown"}
{balance.totalBalance}
Available: {balance.availableBalance}
{parseFloat(balance.lockedBalance) > 0 && (
Locked: {balance.lockedBalance}
)}
))}
);
}
```
## Real-time Balance Updates
```tsx theme={null}
function LiveBalanceTracker({ tokenSymbol }: { tokenSymbol: string }) {
const { balances, subscribed } = useUserBalances();
const balance = balances.find(b => b.symbol === tokenSymbol);
if (!balance) {
return Balance for {tokenSymbol} not found;
}
const lockedPercentage = parseFloat(balance.totalBalance) > 0
? (parseFloat(balance.lockedBalance) / parseFloat(balance.totalBalance)) * 100
: 0;
return (
{subscribed ? "Live Updates Active" : "Disconnected"}
{balance.symbol || "Unknown Token"}
Total Balance: {balance.totalBalance}
Available: {balance.availableBalance}
Locked: {balance.lockedBalance}
{parseFloat(balance.lockedBalance) > 0 && (
Locked: {lockedPercentage.toFixed(2)}%
)}
Decimals: {balance.decimals}
Token: {balance.token.slice(0, 10)}...{balance.token.slice(-8)}
);
}
```
## Error Handling with Retry
```tsx theme={null}
function BalancesWithRetry() {
const { balances, loading, error, clearError, refresh } = useUserBalances();
if (error) {
return (
Failed to load balances
{error.message}
);
}
if (loading) {
return Loading balances...;
}
return (
Balances
{balances.map(balance => (
{balance.symbol}: {balance.totalBalance}
))}
);
}
```
## Complete Dashboard Example
```tsx theme={null}
import { useUserBalances } from "@0xmonaco/react";
function BalancesDashboard() {
const { balances, loading, subscribed, error, refresh } = useUserBalances();
const stats = {
total: balances.length,
active: balances.filter(b => parseFloat(b.totalBalance) > 0).length,
withLocked: balances.filter(b => parseFloat(b.lockedBalance) > 0).length,
};
if (loading) {
return (
);
}
if (error) {
return (
Failed to load balances
{error.message}
);
}
return (
{/* Connection Status */}
{subscribed ? "Real-time updates active" : "Disconnected"}
{/* Stats */}
Total Tokens
{stats.total}
Active Balances
{stats.active}
With Locked
{stats.withLocked}
{/* Balances List */}
Token Balances
{balances.length === 0 ? (
No balances yet
) : (
balances.map(balance => {
const hasBalance = parseFloat(balance.totalBalance) > 0;
const hasLocked = parseFloat(balance.lockedBalance) > 0;
return (
{balance.symbol || "Unknown"}
{hasLocked && (
Locked
)}
Total:
{balance.totalBalance}
Available:
{balance.availableBalance}
{hasLocked && (
Locked:
{balance.lockedBalance}
)}
Decimals: {balance.decimals}
{hasLocked && (
{((parseFloat(balance.lockedBalance) / parseFloat(balance.totalBalance)) * 100).toFixed(2)}% locked
)}
);
})
)}
);
}
export default BalancesDashboard;
```
## AccountBalance Type Structure
```typescript theme={null}
interface AccountBalance {
assetId: string; // Asset UUID (for API calls)
token: string; // Token contract address
symbol: string | null; // Token symbol (e.g., "USDC", "ETH")
decimals: number; // Decimal places for the token
availableBalance: string; // Available balance (normalized, human-readable)
lockedBalance: string; // Locked in orders (normalized, human-readable)
totalBalance: string; // Total balance = available + locked (normalized)
availableBalanceRaw: string; // Available balance in raw format (smallest unit)
lockedBalanceRaw: string; // Locked balance in raw format (smallest unit)
totalBalanceRaw: string; // Total balance = available + locked (raw, smallest unit)
}
```
## See Also
* [useVault](/sdk/react/hooks/use-vault) - Deposit and withdraw tokens
* [useTrade](/sdk/react/hooks/use-trade) - Place and manage orders that lock/unlock balances
* [WebSocket API](/websocket#balance-updates) - Low-level balances WebSocket channel
* [TypeScript SDK](/sdk/typescript/websockets) - Core SDK WebSocket methods
# useUserMovements
Source: https://docs.0xmonaco.com/sdk/react/hooks/use-user-movements
Real-time user balance movements and transaction history
**New in v0.5.4** - Real-time user movement tracking
Subscribe to real-time user ledger movements: deposits, withdrawals, funding settlements, and the balance a cancelled or expired order unlocks. Fills and fees do not arrive on this channel — read them from movement history, and use [`useUserBalances`](/sdk/react/hooks/use-user-balances) for the live balance change a fill produces.
In that history, `balanceBefore` / `balanceAfter` / `lockedBefore` / `lockedAfter` describe a spot balance row. A margin fill leg and a `FUNDING` row have no spot row and report `0` in all four: that is "no spot snapshot", not a wallet balance of zero. Read margin account state for collateral rather than these fields.
```tsx theme={null}
import { useUserMovements } from "@0xmonaco/react";
const {
movements,
loading,
subscribed,
error,
clearError
} = useUserMovements({ maxMovements: 50 });
```
## Parameters
The hook accepts an optional options object.
Maximum number of movements to keep in memory
Movements to fetch on initial load, 1 to 1000. Defaults to `maxMovements` capped at 100. The hook reads one newest-first page and keeps it current from the `movements` channel; it takes no `page`, `pageToken` or `orderBy`. Walk full history with `sdk.profile.getPaginatedUserMovements`.
Filter the initial REST history by entry type. Changing this value re-fetches history. Live WebSocket events are not filtered by the hook.
Filter the initial REST history by transaction type. Changing this value re-fetches history. Live WebSocket events are not filtered by the hook.
Filter the initial REST history by asset UUID. Live WebSocket events are not filtered by the hook.
## Return Values
Array of movement events, newest first. Automatically deduplicated by movement ID.
`true` while fetching initial movements, `false` once loaded
`true` after the channel handler is registered, `false` otherwise. This does not report socket connection or channel delivery state.
Error object if something went wrong, `null` otherwise
Dismisses the current error. It does not retry the request or re-register the handler.
## Features
### Automatic Deduplication
The hook automatically deduplicates movements by ID. If the same movement is received multiple times (e.g., from WebSocket and initial fetch), only one copy is kept.
### Hybrid Data Loading
1. **Initial Load**: Fetches recent movements via REST API
2. **Real-time Updates**: Registers a WebSocket channel handler for new movements
3. **Race Condition Prevention**: WebSocket handler registration only starts after REST data is loaded
### Memory Management
Movements are automatically limited to `maxMovements` entries. Older movements are removed when the limit is exceeded.
## Basic Usage
```tsx theme={null}
import { useUserMovements } from "@0xmonaco/react";
function TransactionHistory() {
const { movements, loading, subscribed, error } = useUserMovements({ maxMovements: 50 });
if (loading) {
return Loading movements...;
}
if (error) {
return Error: {error.message};
}
return (
Movement handler: {subscribed ? "Registered" : "Not registered"}
Recent Movements ({movements.length})
{movements.map(movement => (
{movement.data.transactionType}
{movement.data.amount}
{movement.data.tokenAddress.slice(0, 6)}...
))}
);
}
```
## Filter by Transaction Type
Filters are applied server-side on the initial REST fetch. Changing a filter value automatically re-fetches history, but the hook appends all live WebSocket events. Filter the returned array in the client when the live view must remain restricted. REST history values may be uppercase while WebSocket values may be lowercase, so normalize before comparing.
```tsx theme={null}
function DepositsFeed() {
const { movements } = useUserMovements({
transactionType: "DEPOSIT",
maxMovements: 100,
});
const deposits = movements.filter(
movement => movement.data.transactionType.toUpperCase() === "DEPOSIT"
);
return (
Recent Deposits
{deposits.map(deposit => (
{deposit.data.amount} deposited
{deposit.data.txHash && (
View TX
)}
))}
);
}
```
## Filter by Entry Type and Asset
The REST request uses `assetId`, but the v1.0.51 REST-to-event adapter does not retain that field. To keep the merged history and live feed restricted to one asset, also compare the token address carried by both sources.
```tsx theme={null}
function AssetCredits({
assetId,
tokenAddress,
}: {
assetId: string;
tokenAddress: string;
}) {
const { movements, loading } = useUserMovements({
entryType: "CREDIT",
assetId,
});
const assetCredits = movements.filter(
movement =>
movement.data.entryType.toUpperCase() === "CREDIT" &&
movement.data.tokenAddress.toLowerCase() === tokenAddress.toLowerCase()
);
if (loading) return Loading...;
return (
{assetCredits.map(m => (
{m.data.amount} {m.data.symbol}
))}
);
}
```
## Group by Token
```tsx theme={null}
function BalanceChanges() {
const { movements } = useUserMovements({ maxMovements: 50 });
// Group movements by token address
const byToken = movements.reduce((acc, m) => {
const token = m.data.tokenAddress;
if (!acc[token]) acc[token] = [];
acc[token].push(m);
return acc;
}, {} as Record);
return (
{Object.entries(byToken).map(([token, moves]) => (
{token.slice(0, 6)}...
{moves.length} movements
))}
);
}
```
## Track Order Lifecycle
```tsx theme={null}
function OrderMovements({ orderId }: { orderId: string }) {
const { movements } = useUserMovements({ maxMovements: 100 });
// Find all movements related to this order
const orderMovements = movements.filter(
m => m.data.referenceId === orderId && m.data.referenceType === "order"
);
return (
Order Movements
{orderMovements.map(m => (
{m.data.entryType}: {m.data.amount}
{m.data.description}
))}
);
}
```
## Inspect a Movement Balance Snapshot
`balanceAfter` and `lockedAfter` describe the balance immediately after that movement; they are not a current-wallet-balance API. Use `useUserBalances` or `sdk.profile.getUserBalances()` when you need current balances. A missing snapshot means unavailable data, not zero.
```tsx theme={null}
function LatestBalanceSnapshot({ tokenAddress }: { tokenAddress: string }) {
const { movements } = useUserMovements({ maxMovements: 50 });
const latestMovement = movements.find(
m => m.data.tokenAddress.toLowerCase() === tokenAddress.toLowerCase()
);
if (!latestMovement) return No movement snapshot available
;
const { balanceAfter, lockedAfter } = latestMovement.data;
return (
Available after movement: {balanceAfter ?? "Unavailable"}
Locked after movement: {lockedAfter ?? "Unavailable"}
);
}
```
## Error Handling
```tsx theme={null}
function MovementsWithDismissibleError() {
const { movements, loading, error, clearError } = useUserMovements({ maxMovements: 50 });
if (error) {
return (
Error: {error.message}
);
}
if (loading) {
return Loading...;
}
return (
{movements.map(m => (
{m.data.transactionType}
))}
);
}
```
## Complete Example with Styling
```tsx theme={null}
import { useUserMovements } from "@0xmonaco/react";
import { formatDistanceToNow } from "date-fns";
function MovementsFeed() {
const { movements, loading, subscribed, error } = useUserMovements({ maxMovements: 50 });
const getIcon = (type: string) => {
switch (type.toUpperCase()) {
case "DEPOSIT": return "💰";
case "WITHDRAWAL": return "📤";
case "TRADE": return "🔄";
case "FEE": return "💸";
default: return "📝";
}
};
const getColor = (entryType: string) => {
switch (entryType.toUpperCase()) {
case "CREDIT": return "text-green-600";
case "DEBIT": return "text-red-600";
case "FEE": return "text-orange-600";
default: return "text-gray-600";
}
};
if (loading) {
return (
);
}
if (error) {
return (
Failed to load movements
{error.message}
);
}
return (
{/* Handler registration status; not socket connectivity */}
{subscribed ? "Movement handler registered" : "Movement handler not registered"}
{/* Movements List */}
{movements.length === 0 ? (
No movements yet
) : (
movements.map(movement => (
{getIcon(movement.data.transactionType)}
{movement.data.transactionType}
{movement.data.entryType}: {movement.data.amount}
{movement.data.description && (
{movement.data.description}
)}
{movement.data.createdAt && (
{formatDistanceToNow(new Date(movement.data.createdAt), { addSuffix: true })}
)}
{/* Balance Change */}
{movement.data.balanceBefore && movement.data.balanceAfter && (
Balance: {movement.data.balanceBefore} → {movement.data.balanceAfter}
)}
{/* Transaction Hash */}
{movement.data.txHash && (
View on Explorer →
)}
))
)}
);
}
export default MovementsFeed;
```
## Movement Event Structure
```typescript theme={null}
import type { Address } from "viem";
interface UserMovementEventData {
id: string; // Movement ID (UUID)
entryType: string; // REST values may be uppercase; WebSocket values may be lowercase
transactionType: string; // REST values may be uppercase; WebSocket values may be lowercase
status?: string; // Movement status
token?: Address; // Token contract address
tokenAddress: Address; // Populated legacy alias for token
assetId?: string; // Asset registry ID (UUID)
symbol?: string; // Token symbol (e.g., "BTC", "USDC")
decimals: number; // Token decimals
amount: string; // Human-readable decimal amount
amountRaw?: string; // Optional raw value; funding history may contain a decimal string
balanceBefore?: string; // Balance before movement (decimal)
balanceBeforeRaw?: string; // Balance before (raw)
balanceAfter?: string; // Balance after movement (decimal)
balanceAfterRaw?: string; // Balance after (raw)
lockedBefore?: string; // Locked balance before (decimal)
lockedBeforeRaw?: string; // Locked balance before (raw)
lockedAfter?: string; // Locked balance after (decimal)
lockedAfterRaw?: string; // Locked balance after (raw)
referenceId?: string; // Related entity ID (e.g., order UUID)
referenceType?: string; // Reference type (e.g., "order", "deposit")
description?: string; // Human-readable description
txHash?: string; // On-chain transaction hash
blockNumber?: number; // On-chain block number
createdAt?: string; // ISO 8601 timestamp (UTC)
}
interface UserMovementEvent {
eventType: "movement";
userId: string;
data: UserMovementEventData;
}
```
**Key Fields:**
* **`tokenAddress`**: Required legacy alias populated from `token` (or the older wire alias)
* **`symbol`** (optional): Token ticker symbol (e.g., "BTC", "USDC")
* **`amount`**: Human-readable decimal string and the simplest exact value to display
* **`amountRaw`** (optional): Do not assume this value exists or is an integer. Funding history may contain a decimal string; integer raw-unit conversion applies only after validating a token movement's value.
* **`entryType` and `transactionType`**: REST history may use uppercase values while WebSocket events may use lowercase values. Normalize casing before comparisons.
**Example: Displaying the Exact Amount**
```typescript theme={null}
const movement = movements[0];
if (movement) {
console.log(`Amount: ${movement.data.amount} ${movement.data.symbol ?? ""}`.trim());
}
```
## See Also
* [WebSocket API](/websocket#movement-events) - Low-level movements WebSocket channel
* [TypeScript SDK](/sdk/typescript/websockets#movements-channel) - Core SDK WebSocket methods
* [useProfile](/sdk/react/hooks/use-profile) - User profile and balance queries
# useUserOrders
Source: https://docs.0xmonaco.com/sdk/react/hooks/use-user-orders
Real-time order tracking and updates via WebSocket
**New in v0.5.5** - Real-time order tracking with WebSocket updates
Subscribe to real-time updates for all your orders. Automatically fetches initial orders via REST API and keeps them synchronized with WebSocket events.
```tsx theme={null}
import { useUserOrders } from "@0xmonaco/react";
const {
orders,
loading,
subscribed,
error,
clearError,
refresh
} = useUserOrders(maxOrders);
```
## Parameters
Maximum number of orders to keep in state (configurable history limit)
## Return Values
Array of user orders, automatically updated in real-time. Merged from REST API initial data and WebSocket updates.
`true` while fetching initial orders from REST API, `false` once loaded
`true` when WebSocket subscription is active, `false` otherwise
Error object if something went wrong during fetch or subscription, `null` otherwise
Function to clear the current error state
Reconcile a fresh REST read with live orders, preserving newer live data received while the request was in flight. When the incoming row carries `version`, the hook compares it first: it takes the REST row on `incoming.version >= local.version` and otherwise keeps local state. A versioned local row also wins over an unversioned response. Only when both sides lack `version` does the hook fall back to `updatedAt`; it retains live-only rows before applying the `maxOrders` limit.
## Features
### Automatic Deduplication
Orders are automatically deduplicated by order ID. If the same order appears in both REST API response and WebSocket updates, only one copy is kept with the most recent data.
### Hybrid Data Loading
1. **Initial Load**: fetches recent orders via REST (`getPaginatedOrders`).
2. **WebSocket Subscription**: subscribes to the `orders` channel after the initial load.
3. **Race Condition Prevention**: the subscription only opens once the REST data is in, so an event cannot be overwritten by the response it raced.
4. **Subscribe-time Snapshot**: the channel's [snapshot](/sdk/typescript/websockets#subscribe-time-snapshots) carries every **resting** order, and the hook reconciles it into the list.
### Applying the snapshot
The snapshot lists persisted resting orders only, so it is not the whole list this hook shows. It reconciles rows it carries directly, while an omitted locally resting row needs a point read:
* A snapshot row is ranked against the row already held on `version`. It wins on `snapshot.version >= local.version`; a lower or absent snapshot version cannot overwrite versioned local state left by the previous connection. When both versions are absent, the snapshot keeps its pre-counter precedence without comparing incompatible timestamps.
* A row still showing `SUBMITTED` or `PARTIALLY_FILLED` that the snapshot **omits** is retained while the hook calls `sdk.trading.getOrder(orderId)`. Absence alone is ambiguous: the order may have filled or been cancelled during the outage, or persistence may lag a newer resting event the hook retained from the previous connection. Versioned results use the same ordering rule. When both rows are unversioned, the point result is accepted only if the held row did not change while the request was in flight; otherwise the hook preserves that intervening state and retries if it still needs resolution.
* Terminal orders are untouched. The snapshot never carried them, so their absence says nothing.
**`selfTradePreventionMode` is unreliable on this hook, as of v1.0.60.** Rows seeded from REST carry it — the hook's initial `getPaginatedOrders` load and its `getOrder` repairs both return it on `Order`. But `orderFromEvent` and `orderFromSnapshotItem` do not map it through, so a row that first appears from a live event or a subscribe-time snapshot omits it even though the underlying frame carries the value. Absence is therefore **not** evidence the order carried no override — unlike `postOnly`, where absence is meaningful. Where the requested mode matters, read it from `sdk.trading.getOrder` or the core `sdk.ws.userOrders` stream rather than from a row this hook built from stream data.
* A snapshot row is **layered** over the order it supersedes, not substituted for it, so fields the hook does not take from the row — `postOnly`, `expirationDate`, `positionSide`, `terminalReason`, `conditionalOrderId` and the fee aggregates — survive.
`conditionalOrderId` — the take-profit or stop-loss conditional that placed a triggered close — is REST-only in the strictest sense: no order event and no snapshot row carries it, so a close this connection saw first over the socket carries no value for it. The hook does not schedule a read just because the field is missing — an order added from an `OrderPlaced` event is not enqueued for repair, and snapshot reconciliation leaves terminal rows alone — so the field appears only when some REST read of that order happens anyway: your own refetch, or one of the hook's existing repair reads. Absent on orders the user placed, and on closes written before the field existed.
The fee aggregates are the one thing that can go stale rather than missing, because they accumulate with fills. A snapshot reporting a fill this connection never saw schedules the same debounced REST re-read the hook already uses, which is their only source.
If a snapshot row carries a future enum value this SDK cannot render, its validated `orderId` still marks that order present. The hook schedules a broad compatibility re-read for that row while continuing targeted resolution for distinct locally resting orders the snapshot omitted.
The broad REST repair is debounced and page-limited, but snapshot omissions use targeted reads because an older order may no longer fit on the newest list page. Those reads are deduplicated across repeated snapshots, limited to four in flight, and applied as each one succeeds, so a slow order does not delay its siblings. Each attempt releases its queue slot after ten seconds. If a point read fails or times out, the hook keeps the local row rather than treating that as proof the order is terminal; a later snapshot can retry it.
### Real-time Updates
The hook listens to all order events:
* `OrderPlaced` - New orders added to state. A new row starts at `filledQuantity: "0"` with no `averageFillPrice`, except the replacement of a partly filled order (an edit): it starts from the fill and average its `OrderPlaced` carries over from the original, as `getOrder` reports it
* `OrderPartiallyFilled` - Order updated with fill amount
* `OrderFilled` - Order marked as fully filled
* `OrderCancelled` - Order marked as cancelled
* `OrderRejected` - Order marked as rejected
* `OrderExpired` - Order marked as expired
### Authentication Required
This hook requires an authenticated session. Log in with the [`useAuth`](/sdk/react/hooks/use-auth) hook (or `sdk.login(...)`) before using it.
## Basic Usage
```tsx theme={null}
import { useUserOrders } from "@0xmonaco/react";
function OrdersList() {
const { orders, loading, subscribed, error } = useUserOrders(50);
if (loading) {
return Loading orders...;
}
if (error) {
return Error: {error.message};
}
return (
WebSocket: {subscribed ? "Connected" : "Disconnected"}
Your Orders ({orders.length})
{orders.map(order => (
{order.side} {order.quantity} @ {order.price}
Status: {order.status}
))}
);
}
```
## Filter Active Orders
```tsx theme={null}
function ActiveOrders() {
const { orders } = useUserOrders(100);
const activeOrders = orders.filter(
order => order.status === "SUBMITTED" || order.status === "PARTIALLY_FILLED"
);
return (
Active Orders ({activeOrders.length})
{activeOrders.map(order => (
{order.side} {order.quantity} @ {order.price}
{order.filledQuantity && order.filledQuantity !== "0" && (
Filled: {order.filledQuantity}
)}
))}
);
}
```
## Group by Trading Pair
```tsx theme={null}
function OrdersByPair() {
const { orders } = useUserOrders(100);
// Group by trading pair ID. To display human-readable symbols, fetch
// trading pairs once via sdk.market.getPaginatedTradingPairs and build
// (or maintain) a local id → symbol map.
const byPair = orders.reduce((acc, order) => {
const pair = order.tradingPairId;
if (!acc[pair]) acc[pair] = [];
acc[pair].push(order);
return acc;
}, {} as Record);
return (
{Object.entries(byPair).map(([pair, pairOrders]) => (
{pair}
{pairOrders.length} orders
{pairOrders.map(order => (
-
{order.side} {order.quantity} @ {order.price} - {order.status}
))}
))}
);
}
```
## Order History with Filters
```tsx theme={null}
function OrderHistory() {
const { orders, loading } = useUserOrders(200);
const [filter, setFilter] = useState<"all" | "active" | "filled" | "cancelled">("all");
const filteredOrders = orders.filter(order => {
switch (filter) {
case "active":
return order.status === "SUBMITTED" || order.status === "PARTIALLY_FILLED";
case "filled":
return order.status === "FILLED" || order.status === "SETTLED" || order.status === "SETTLED_ON_CHAIN";
case "cancelled":
return order.status === "CANCELLED";
default:
return true;
}
});
return (
{loading ? (
Loading...
) : (
{filteredOrders.map(order => (
{order.tradingPairId}
{order.side} {order.quantity}
{order.price && @ {order.price}}
{order.status}
))}
)}
);
}
```
## Real-time Order Updates
```tsx theme={null}
function LiveOrderTracker({ orderId }: { orderId: string }) {
const { orders, subscribed } = useUserOrders(50);
const order = orders.find(o => o.id === orderId);
if (!order) {
return Order not found;
}
const fillPercentage = order.filledQuantity && order.quantity
? (parseFloat(order.filledQuantity) / parseFloat(order.quantity)) * 100
: 0;
return (
Live: {subscribed ? "Connected" : "Disconnected"}
Order {order.id.slice(0, 8)}...
Status: {order.status}
Side: {order.side}
Quantity: {order.quantity}
{order.price && Price: {order.price}
}
{order.filledQuantity && order.filledQuantity !== "0" && (
Filled: {order.filledQuantity} ({fillPercentage.toFixed(2)}%)
)}
{order.averageFillPrice && (
Average Fill Price: {order.averageFillPrice}
)}
);
}
```
## Error Handling with Retry
```tsx theme={null}
function OrdersWithRetry() {
const { orders, loading, error, clearError, refresh } = useUserOrders(50);
if (error) {
return (
Failed to load orders
{error.message}
);
}
if (loading) {
return Loading orders...;
}
return (
{orders.map(order => (
{order.tradingPairId}: {order.side} {order.quantity}
))}
);
}
```
## Complete Dashboard Example
```tsx theme={null}
import { useUserOrders } from "@0xmonaco/react";
import { formatDistanceToNow } from "date-fns";
function OrdersDashboard() {
const { orders, loading, subscribed, error } = useUserOrders(100);
const stats = {
total: orders.length,
active: orders.filter(o => o.status === "SUBMITTED" || o.status === "PARTIALLY_FILLED").length,
filled: orders.filter(o => o.status === "FILLED" || o.status === "SETTLED" || o.status === "SETTLED_ON_CHAIN").length,
cancelled: orders.filter(o => o.status === "CANCELLED").length,
};
const getStatusColor = (status: string) => {
switch (status) {
case "SUBMITTED":
case "PARTIALLY_FILLED":
return "text-blue-600";
case "FILLED":
case "SETTLED":
case "SETTLED_ON_CHAIN":
return "text-green-600";
case "CANCELLED":
return "text-gray-600";
case "REJECTED":
return "text-red-600";
default:
return "text-gray-600";
}
};
if (loading) {
return (
);
}
if (error) {
return (
Failed to load orders
{error.message}
);
}
return (
{/* Connection Status */}
{subscribed ? "Real-time updates active" : "Disconnected"}
{/* Stats */}
Total Orders
{stats.total}
Active
{stats.active}
Filled
{stats.filled}
Cancelled
{stats.cancelled}
{/* Orders List */}
{orders.length === 0 ? (
No orders yet
) : (
orders.map(order => (
{order.tradingPairId}
{order.side}
{order.status}
Quantity: {order.quantity}
{order.price && Price: {order.price}}
{order.filledQuantity && order.filledQuantity !== "0" && (
Filled: {order.filledQuantity}
)}
{order.averageFillPrice && (
Avg Fill Price: {order.averageFillPrice}
)}
{order.createdAt && (
{formatDistanceToNow(new Date(order.createdAt), { addSuffix: true })}
)}
))
)}
);
}
export default OrdersDashboard;
```
## Order Type Structure
```typescript theme={null}
interface Order {
id: string; // Order UUID
tradingPairId: string; // Trading pair UUID
orderType: OrderType; // "LIMIT" | "MARKET"
side: OrderSide; // "BUY" | "SELL"
price?: string; // Order price (undefined for MARKET orders)
quantity: string; // Order quantity
filledQuantity: string; // Amount filled so far
averageFillPrice?: string; // Average price of fills
status: OrderStatus; // Order status
tradingMode: TradingMode; // "SPOT" | "MARGIN"
timeInForce?: TimeInForce; // "GTC" | "IOC" | "FOK"; absent on MARKET orders, which have none
clientOrderId?: string; // correlation handle echoed back as supplied; absent when the order carried none
selfTradePreventionMode?: "CANCEL_MAKER" | "CANCEL_TAKER" | "CANCEL_BOTH" | "SKIP"; // the per-order override REQUESTED at placement — present on REST-seeded rows, absent on event/snapshot-derived ones (see the limitation below)
createdAt: string; // ISO 8601 timestamp (UTC)
updatedAt?: string; // ISO 8601 timestamp (UTC)
version?: number; // opaque same-order revision; absent means unknown
expirationDate?: string; // expiry for a GTC order
conditionalOrderId?: string; // REST-only: the TP/SL conditional that placed this order
parentOrderId?: string; // TWAP child only: the TWAP parent that placed it (REST-seeded rows)
// Perp-only fields (populated when this is a margin order)
marginAccountId?: string;
positionSide?: "LONG" | "SHORT" | "NONE";
leverage?: string;
reduceOnly?: boolean;
postOnly?: boolean; // maker-only; absent for orders placed before the flag was persisted
positionId?: string;
}
type OrderStatus =
| "SUBMITTED"
| "PARTIALLY_FILLED"
| "FILLED"
| "CANCELLED"
| "REJECTED"
| "EXPIRED"
| "SETTLED"
| "SETTLED_ON_CHAIN"; // terminal state — fully settled on-chain
```
## See Also
* [useTrade](/sdk/react/hooks/use-trade) - Place, cancel, and manage orders
* [useOrderbook](/sdk/react/hooks/use-orderbook) - Real-time orderbook updates
* [WebSocket API](/websocket#subscribe-to-all-user-orders) - Low-level userOrders WebSocket channel
* [TypeScript SDK](/sdk/typescript/websockets#orders-channel) - Core SDK WebSocket methods
# useVault
Source: https://docs.0xmonaco.com/sdk/react/hooks/use-vault
Deposit and withdraw tokens
```tsx theme={null}
import { useVault } from "@0xmonaco/react";
const {
approve,
deposit,
withdraw,
retryWithdrawal,
listPendingWithdrawals,
getAllowance,
needsApproval
} = useVault();
```
## Vault Operations
Deposit tokens into vault. Use asset UUID from `sdk.profile.getUserBalances()`.
Withdraw tokens from vault. Use asset UUID from `sdk.profile.getUserBalances()`. `source` is `"spot"` or `"margin"`; see [`sdk.vault.withdraw`](/sdk/typescript/vault).
Retry the on-chain submission of a withdrawal that never landed. It does **not** create a new withdrawal; see [`sdk.vault.retryWithdrawal`](/sdk/typescript/vault).
List the caller's withdrawals still awaiting on-chain confirmation, newest first, cursor-paginated with `pageSize` and `pageToken`. Wraps [`sdk.withdrawals.listPendingWithdrawals`](/sdk/typescript/vault).
## Token Approvals
Approve vault to spend tokens. Use asset UUID from trading pairs.
Check current token allowance by asset UUID
Check if approval is needed for an asset UUID
## Validation Requirements
All vault operations include built-in validation to prevent common errors:
**Required Parameters:** Asset ID must be a non-empty string, and amount must be greater than 0.
**Validation Checks:**
| Operation | Validation | Error Message |
| - | - | - |
| `deposit()` | SDK available | "SDK not available" |
| | Asset ID non-empty | "Asset ID is required and cannot be empty" |
| | Amount > 0n | "Amount must be greater than 0" |
| `withdraw()` | SDK available | "SDK not available" |
| | Asset ID non-empty | "Asset ID is required and cannot be empty" |
| | Amount > 0n | "Amount must be greater than 0" |
| `retryWithdrawal()` | SDK available | "SDK not available" |
| | Index is a non-negative integer | "withdrawalIndex must be a non-negative integer" |
| `listPendingWithdrawals()` | SDK available | "SDK not available" |
| `approve()` | SDK available | "SDK not available" |
| | Asset ID non-empty | "Asset ID is required and cannot be empty" |
| | Amount > 0n | "Amount must be greater than 0" |
| `getAllowance()` | SDK available | "SDK not available" |
| | Asset ID non-empty | "Asset ID is required and cannot be empty" |
| `needsApproval()` | SDK available | "SDK not available" |
| | Asset ID non-empty | "Asset ID is required and cannot be empty" |
| | Amount > 0n | "Amount must be greater than 0" |
**Example with error handling:**
```typescript theme={null}
import { useVault } from "@0xmonaco/react";
function DepositButton({ assetId, amount }: { assetId: string; amount: bigint }) {
const { deposit } = useVault();
const handleDeposit = async () => {
try {
// Validation happens automatically
const result = await deposit(assetId, amount);
console.log('Deposit successful:', result.transactionHash);
} catch (error) {
// Handle validation errors
if (error.message.includes('Asset ID is required')) {
alert('Please select an asset');
} else if (error.message.includes('Amount must be greater')) {
alert('Please enter a valid amount');
} else if (error.message.includes('SDK not available')) {
alert('SDK is still loading, please wait');
} else {
console.error('Deposit failed:', error);
}
}
};
return ;
}
```
# React SDK
Source: https://docs.0xmonaco.com/sdk/react/intro
@0xmonaco/react
The Monaco React SDK provides `MonacoProvider` and React hooks for authentication, trading, vault operations, market data, and real-time WebSocket feeds.
## Installation
```bash npm theme={null}
npm install @0xmonaco/react @0xmonaco/core wagmi viem
```
```bash pnpm theme={null}
pnpm add @0xmonaco/react @0xmonaco/core wagmi viem
```
```bash yarn theme={null}
yarn add @0xmonaco/react @0xmonaco/core wagmi viem
```
## Basic Setup
`MonacoProvider` initializes the SDK and makes Monaco hooks available to every child component. During private alpha, configure wallets for Sei testnet and use `network="staging"`.
```tsx Wagmi theme={null}
import { MonacoProvider } from "@0xmonaco/react";
import { WagmiProvider, createConfig, http } from "wagmi";
import { seiTestnet } from "wagmi/chains";
const wagmiConfig = createConfig({
chains: [seiTestnet],
transports: {
[seiTestnet.id]: http()
}
});
export function App() {
return (
);
}
```
```tsx Wallet Client theme={null}
import { MonacoProvider } from "@0xmonaco/react";
import { createWalletClient, custom } from "viem";
import { seiTestnet } from "viem/chains";
function App({ provider }: { provider: EIP1193Provider }) {
const walletClient = createWalletClient({
chain: seiTestnet,
transport: custom(provider)
});
return (
);
}
```
## Configuration Options
Your Monaco application client ID.
Monaco network to connect to. Use `"staging"` for Sei testnet during private alpha. Mainnet support will use the mainnet network when access opens.
Sei RPC endpoint URL used for on-chain reads and transaction confirmation. During private alpha, use a Sei testnet RPC. In production code, source `seiRpcUrl` from an environment variable rather than hardcoding it.
Optional [Viem `WalletClient`](https://viem.sh/docs/clients/wallet) used to sign the authorization that creates the Monaco session key during `login()`. Pass this when your wallet connection is outside Wagmi's connector context, such as Privy, ConnectKit, RainbowKit without Wagmi context, or a hand-built Viem wallet client.
Optional configuration for automatic session management. Despite the name, this manages the wallet-authorized ed25519 session-key lifecycle.
* `autoRefresh`: Extend the session before it expires (default: `true`)
* `persistTokens`: Store the session in localStorage so it survives reloads (default: `true`)
* `refreshBufferSeconds`: Seconds before expiry to trigger a refresh (default: `300`)
* `onRefreshError`: Callback when a session refresh fails
* `onRefreshSuccess`: Callback when a session refresh succeeds
Your application components.
## Accessing the SDK
Use `useMonacoSDK` to access the underlying SDK instance from any child component.
```tsx theme={null}
import { useMonacoSDK } from "@0xmonaco/react";
function SDKStatus() {
const { sdk, error, clearError } = useMonacoSDK();
if (error) {
return ;
}
return sdk ? "Ready" : "Initializing...";
}
```
## Session Persistence
By default, the provider stores the Monaco session in `localStorage` and restores it on reload. The persisted session includes the ed25519 session private key used to sign requests.
`localStorage` is exposed to XSS. Harden production apps with a Content Security Policy, or disable persistence for security-sensitive apps.
```tsx theme={null}
```
# Rust SDK
Source: https://docs.0xmonaco.com/sdk/rust-and-bsr
Choose between Monaco's REST and gRPC Rust clients
Monaco ships two official Rust clients on [crates.io](https://crates.io): one generated from the REST OpenAPI spec and one generated from the gRPC protobuf module. Choose based on how your backend talks to Monaco.
## Choose a Rust Client
| Client | Crate | Transport | Use when |
| - | - | - | - |
| REST SDK | [`monaco-sdk`](https://crates.io/crates/monaco-sdk) | HTTP/JSON | You want broad compatibility, simple request/response calls, and OpenAPI-generated types. |
| gRPC SDK | [`monaco-grpc-sdk`](https://crates.io/crates/monaco-grpc-sdk) | gRPC over HTTP/2 | You want protobuf-native clients, service stubs, and the lowest-latency backend integration path. |
Use `monaco-sdk` for REST market data, authenticated requests, request signing helpers, and order workflows over HTTP/JSON.
Use `monaco-grpc-sdk` for protobuf-generated service clients over gRPC.
## Hosts
```text REST theme={null}
https://staging.apimonaco.xyz/api
```
```text gRPC theme={null}
https://staging.apimonaco.xyz
```
Authenticated trading, account, deposit, and withdrawal workflows require a Monaco client ID and a wallet-authorized session key. Public market data can be read without authentication.
# Rust gRPC SDK
Source: https://docs.0xmonaco.com/sdk/rust-grpc
Get started with Monaco's protobuf-generated Rust gRPC SDK
`monaco-grpc-sdk` is generated from Monaco's protobuf module published to the Buf Schema Registry. Use it when you want protobuf-native service clients over gRPC.
## Add Dependencies
```toml theme={null}
[dependencies]
monaco-grpc-sdk = "1.0"
tonic = { version = "0.14", features = ["transport", "tls-ring", "tls-webpki-roots"] }
tokio = { version = "1", features = ["full"] }
# Request signing for authenticated calls (see "Authenticate requests").
ed25519-dalek = "2"
prost = "0.14"
sha2 = "0.10"
hex = "0.4"
uuid = { version = "1", features = ["v4"] }
```
See [`crates.io/crates/monaco-grpc-sdk`](https://crates.io/crates/monaco-grpc-sdk) for the published crate.
### Optional `serde` feature
Turn on the `serde` feature to derive `serde::Serialize` and `serde::Deserialize` on every generated message and enum, in the same JSON shape the REST API (`/api/v1/...`) sends and accepts:
```toml theme={null}
monaco-grpc-sdk = { version = "1.0", features = ["serde"] }
```
* Field names are camelCase, and the REST snake\_case aliases are accepted on input.
* The uint64 lease and quote fences (`generation`, `quoteRevision`, `expectedGeneration`) are decimal strings.
* An unset optional field is omitted where REST omits it and serialized as `null` elsewhere (an unset `Position.opened_at` serializes as `"openedAt": null`). A key the server omits reads as unset.
* The request types that REST refuses unknown fields on refuse them here too.
The feature is off by default and changes nothing on the gRPC wire.
## Connect and List Pairs
The gRPC service is exposed on the same staging host as the REST API. Connect to the host on port `443`; the load balancer routes gRPC method paths such as `/monaco.api.*` to Monaco's gRPC API.
```rust theme={null}
use monaco_grpc_sdk::monaco::api::market::{
market_service_client::MarketServiceClient,
ListTradingPairsRequest,
};
use tonic::transport::{Channel, ClientTlsConfig};
#[tokio::main]
async fn main() -> Result<(), Box> {
let channel = Channel::from_static("https://staging.apimonaco.xyz")
.tls_config(ClientTlsConfig::new().with_webpki_roots())?
.connect()
.await?;
let mut market = MarketServiceClient::new(channel);
let response = market
.list_trading_pairs(ListTradingPairsRequest {
page: Some(1),
page_size: Some(20),
market_type: None,
base_token: None,
quote_token: None,
is_active: Some(true),
})
.await?
.into_inner();
for pair in response.trading_pairs {
println!("{} {}", pair.symbol, pair.id);
}
Ok(())
}
```
## Authenticate requests
Market data is public. Trading and account RPCs are **tokenless**: sign every
request with the ed25519 **session key** you registered through
`AuthService.Challenge`/`Verify`, and attach the signature as metadata. The
server verifies it against your stored public key.
The signature covers the canonical string `POST\n\n\nSHA256(request bytes)`,
where the request bytes are the **prost-encoded message** — the same bytes tonic
sends — so hash `msg.encode_to_vec()` and bind the full leading-slash method
path. Add `ed25519-dalek`, `prost`, `sha2`, and `hex` alongside the SDK.
The raw generated client keeps `idempotency_key` optional and does not generate it. Create and retain the key before protobuf encoding or signing. Persist the prepared message and key before sending if recovery must survive a process restart.
```rust theme={null}
use ed25519_dalek::{Signer, SigningKey};
use monaco_grpc_sdk::monaco::api::orders::{
orders_service_client::OrdersServiceClient, CreateOrderRequest,
};
use prost::Message;
use sha2::{Digest, Sha256};
use std::time::{SystemTime, UNIX_EPOCH};
use tonic::transport::{Channel, ClientTlsConfig};
use tonic::Request;
/// Wrap a request with the three `x-monaco-*` signing metadata entries.
fn signed_request(msg: T, method_path: &str, key: &SigningKey) -> Request {
let hash = hex::encode(Sha256::digest(msg.encode_to_vec()));
// Real current time in milliseconds; a stale or far-future value is rejected.
let timestamp = SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("system clock before epoch")
.as_millis()
.to_string();
let signing_string = format!("POST\n{method_path}\n{timestamp}\n{hash}");
let signature = hex::encode(key.sign(signing_string.as_bytes()).to_bytes());
let public_key = hex::encode(key.verifying_key().to_bytes());
let mut request = Request::new(msg);
let md = request.metadata_mut();
md.insert("x-monaco-publickey", public_key.parse().expect("ascii metadata"));
md.insert("x-monaco-timestamp", timestamp.parse().expect("ascii metadata"));
md.insert("x-monaco-signature", signature.parse().expect("ascii metadata"));
request
}
#[tokio::main]
async fn main() -> Result<(), Box> {
let channel = Channel::from_static("https://staging.apimonaco.xyz")
.tls_config(ClientTlsConfig::new().with_webpki_roots())?
.connect()
.await?;
// ed25519 session key registered via AuthService.Challenge/Verify (32-byte
// hex seed). The server verifies against its public key — there is no token.
let seed: [u8; 32] = hex::decode(std::env::var("MONACO_SESSION_SEED")?)?
.try_into()
.expect("MONACO_SESSION_SEED must be 32-byte hex");
let key = SigningKey::from_bytes(&seed);
let mut orders = OrdersServiceClient::new(channel);
const CREATE_ORDER: &str = "/monaco.api.orders.OrdersService/CreateOrder";
let submission_key = uuid::Uuid::new_v4().to_string();
let create = CreateOrderRequest {
idempotency_key: Some(submission_key),
trading_pair_id: "TRADING_PAIR_UUID".into(), // from MarketService.ListTradingPairs
order_type: "LIMIT".into(),
side: "BUY".into(),
quantity: "0.01".into(),
price: Some("60000".into()),
trading_mode: Some("SPOT".into()),
time_in_force: Some("GTC".into()),
..Default::default()
};
// Save `create` before sending; retain it for recovery.
let response = orders
.create_order(signed_request(create.clone(), CREATE_ORDER, &key))
.await?
.into_inner();
println!("created order: {response:?}");
Ok(())
}
```
For a lost create response, re-sign `create.clone()` with a fresh timestamp; keep the same payload and key. Give every batch-create item a distinct key before encoding and signing the batch. Protection lasts 24 hours after acceptance and retries do not extend it. Do not retry an unresolved submission after expiry. This example does not retry automatically.
Each call re-signs with a fresh timestamp and body hash, so signatures are never
reused. A `UNAUTHENTICATED` status on a previously working call means the session
expired or was revoked — re-run challenge/verify and retry; do not loop against a
persistent `UNAUTHENTICATED`. The [authenticated gRPC quickstart](/reference/grpc#authenticate-requests)
covers the metadata, error handling, and [golden signing vectors](/reference/grpc#golden-signing-vectors)
you can check a Rust signer against.
## Release Channels
| Version pattern | Source | Purpose |
| - | - | - |
| `X.Y.Z` | every SDK release on `main` | Latest stable |
| `X.Y.Z-prerelease.` | a `release/*` branch | Release candidate, opt in via exact pin |
| `X.Y.Z-develop.` | every SDK-affecting merge to `develop` | Dev snapshot, opt in via exact pin |
Release candidates and dev snapshots use a SemVer pre-release suffix, so a default `cargo add monaco-grpc-sdk` ignores them. Pin explicitly to opt in:
```toml theme={null}
monaco-grpc-sdk = "=1.0.74-develop.367860f"
```
See the [gRPC Protos reference](/grpc/protos) for the complete service list and message schemas.
# Rust REST SDK
Source: https://docs.0xmonaco.com/sdk/rust-rest
Get started with Monaco's HTTP/JSON Rust SDK
`monaco-sdk` is generated from Monaco's OpenAPI spec and covers the REST API surface. Use it when you want HTTP/JSON compatibility and typed Rust methods for market data, account data, and trading workflows.
## Add Dependencies
```toml theme={null}
[dependencies]
monaco-sdk = "1.0"
alloy = { version = "1", features = ["signers", "signer-local"] }
reqwest = { version = "0.13", features = ["json"] }
tokio = { version = "1", features = ["full"] }
ed25519-dalek = "2"
sha2 = "0.10"
rand = "0.8"
serde_json = "1"
uuid = { version = "1", features = ["v4"] }
```
See [`crates.io/crates/monaco-sdk`](https://crates.io/crates/monaco-sdk) for the published crate.
## Market Data
No authentication is required to read trading pairs.
```rust theme={null}
#[tokio::main]
async fn main() -> Result<(), Box> {
let client = monaco_sdk::Client::new("https://staging.apimonaco.xyz");
let pairs = client
.list_trading_pairs(None, Some(true), None, None, None, None)
.await?
.into_inner();
let pairs = pairs.trading_pairs.unwrap_or_default();
for pair in pairs.iter().take(5) {
println!("{}", pair.symbol.as_deref().unwrap_or("?"));
}
let sei_pair = pairs
.iter()
.find(|pair| pair.symbol.as_deref() == Some("SEI/USDC"))
.expect("SEI/USDC pair not found");
let pair_id = sei_pair.id.unwrap();
```
## Authenticate
Monaco authenticates with a wallet-authorized ed25519 **session key**. Generate a keypair, have the wallet sign a challenge that commits to it, then sign every authenticated request with the session key.
```rust theme={null}
use alloy::hex;
use alloy::signers::local::PrivateKeySigner;
use ed25519_dalek::{Signer as _, SigningKey};
use rand::rngs::OsRng;
use serde_json::json;
let signer: PrivateKeySigner = "0xYOUR_PRIVATE_KEY".parse()?;
let address = format!("{:?}", signer.address());
let base = "https://staging.apimonaco.xyz";
let api_prefix = "/api/v1";
let http = reqwest::Client::new();
let session_key = SigningKey::generate(&mut OsRng);
let session_public_key = hex::encode(session_key.verifying_key().to_bytes());
let challenge: serde_json::Value = http
.post(format!("{base}{api_prefix}/auth/challenge"))
.json(&json!({
"address": address,
"clientId": "YOUR_CLIENT_ID",
"sessionPublicKey": session_public_key,
}))
.send().await?.json().await?;
let message = challenge["message"].as_str().unwrap();
let nonce = challenge["nonce"].as_str().unwrap();
let sig = alloy::signers::Signer::sign_message(&signer, message.as_bytes()).await?;
let sig_hex = format!("0x{}", hex::encode(sig.as_bytes()));
http.post(format!("{base}{api_prefix}/auth/verify"))
.json(&json!({
"address": address,
"clientId": "YOUR_CLIENT_ID",
"nonce": nonce,
"signature": sig_hex,
"sessionPublicKey": session_public_key,
}))
.send().await?.error_for_status()?;
```
## Sign Authenticated Requests
On native targets, the SDK can sign session-authenticated requests automatically:
```rust theme={null}
use monaco_sdk::{Client, RequestSigning, SessionSigner};
let client = Client::new_authenticated(
base,
SessionSigner::new(session_key.to_bytes(), RequestSigning::Rfc9421),
);
```
Use your registered session signing key as `session_key`. RFC 9421 signing is opt-in and requires a server that supports it. Use `RequestSigning::Legacy` for existing servers. The authenticated client disables redirects; public operations remain unsigned. See [HTTP message signatures](/reference/http-message-signatures) for the signing profile and migration steps.
The manual example below uses legacy signing. Legacy authenticated REST requests carry `X-Monaco-PublicKey`, `X-Monaco-Timestamp`, and `X-Monaco-Signature`. The signature covers:
```text theme={null}
METHOD
path?query
timestamp_ms
SHA256_hex(body)
```
```rust theme={null}
use sha2::{Digest, Sha256};
use std::time::{SystemTime, UNIX_EPOCH};
async fn signed_post(
http: &reqwest::Client,
base: &str,
api_prefix: &str,
key: &SigningKey,
public_key: &str,
path: &str,
body: &serde_json::Value,
) -> Result> {
let request_path = format!("{api_prefix}{path}");
let body_bytes = serde_json::to_vec(body)?;
let ts = SystemTime::now().duration_since(UNIX_EPOCH)?.as_millis().to_string();
let body_hash = hex::encode(Sha256::digest(&body_bytes));
let signing_string = format!("POST\n{request_path}\n{ts}\n{body_hash}");
let signature = hex::encode(key.sign(signing_string.as_bytes()).to_bytes());
let resp = http
.post(format!("{base}{request_path}"))
.header("X-Monaco-PublicKey", public_key)
.header("X-Monaco-Timestamp", ts)
.header("X-Monaco-Signature", signature)
.header("Content-Type", "application/json")
.body(body_bytes)
.send().await?
.error_for_status()?;
Ok(resp.json().await?)
}
```
## Place a Limit Order
The raw generated client keeps `idempotency_key` optional and does not generate it. Set it yourself (`idempotencyKey` in JSON) before serialization and signing. Retain the prepared payload and key; persist them before sending if recovery must survive a process restart.
```rust theme={null}
let submission_key = uuid::Uuid::new_v4().to_string();
let create = json!({
"idempotencyKey": submission_key,
"tradingPairId": pair_id,
"orderType": "LIMIT",
"side": "BUY",
"price": "0.0100",
"quantity": "0.1",
"timeInForce": "GTC",
});
// Save `create` before sending. A recovery attempt reuses this payload.
let order = signed_post(&http, base, api_prefix, &session_key, &session_public_key, "/orders", &create).await?;
let order_id = order["orderId"].as_str().unwrap().to_string();
println!("order {order_id}: {}", order["status"].as_str().unwrap_or("?"));
```
For a lost response, reuse `create` unchanged and sign again with a fresh timestamp. Use a distinct key for each batch-create item, generated before signing the batch. The key protects the original submission for 24 hours after acceptance; retries do not extend that window. Do not retry an unresolved submission after expiry. This example does not retry automatically.
A `GTC` order rests on the book until filled or cancelled. See [Order Management](/developers/trade) for other time-in-force options.
## Cancel an Order
```rust theme={null}
let cancel = signed_post(&http, base, api_prefix, &session_key, &session_public_key, "/orders/cancel", &json!({
"orderId": order_id,
})).await?;
println!("cancel: {}", cancel["status"].as_str().unwrap_or("?"));
Ok(())
}
```
# Applications
Source: https://docs.0xmonaco.com/sdk/typescript/applications
Application configuration and public reporting
`getApplicationConfig` is session-authenticated (call it after a user logs in). The five reporting methods are public: they need no session or key, and each selects its application by `clientId` — the same public client ID your frontend logs in with. An unknown or inactive client ID returns `404`.
The reporting methods are rate limited per client IP: 60 requests per minute sustained, with a burst of 120. Over the limit the API returns `429` with a `Retry-After` header.
```typescript theme={null}
import { MonacoSDK } from "@0xmonaco/core";
// Session auth (after user login)
await sdk.applications.getApplicationConfig();
// Public reporting: select the application by client ID
const clientId = "6fe0da813f1a4adcbe675d52ca530a43";
await sdk.applications.listApplicationOrders({ clientId, status: "FILLED" });
await sdk.applications.listApplicationUsers({ clientId, isActive: true });
await sdk.applications.listApplicationMovements({ clientId, transactionType: "TRADE" });
await sdk.applications.listApplicationBalances({ clientId, assetId: "..." });
await sdk.applications.getApplicationStats({ clientId, since: "2026-01-01T00:00:00Z" });
```
Retrieve application configuration for the current client ID. Session-authenticated.
**Returns:** `ApplicationConfigResponse`:
* `id`: string — application UUID
* `name`: string — application name
* `allowedOrigins`: string\[] — CORS-allowed origins
* `webhookUrl?`: string — webhook URL for event delivery
* `vaultContractAddress`: string — vault contract address for this application
* `clientId`: string — application client ID, embedded in on-chain deposit calls
List orders placed by the application's users. Public.
**Parameters:**
* `params`: `ListAppOrdersParams`
* `clientId`: string — the application's public client ID (required)
* `pageSize?`: number — items per page, 1 to 1000 (default 20); a larger value is rejected
* `pageToken?`: string — cursor pagination. Omitted or `""` starts the walk; otherwise pass the previous response's `nextPageToken`. There is no `page`: the SDK rejects one with a `ValidationError` before sending, and a raw REST request carrying it gets `400`. The filters must match the request that minted the token
* `status?`: string — filter by order status (e.g. `SUBMITTED`, `PARTIALLY_FILLED`, `FILLED`, `SETTLED`, `CANCELLED`, `EXPIRED`, `REJECTED`)
* `userId?`: string — filter by user UUID
* `tradingPairId?`: string — filter by trading pair UUID
* `side?`: string — filter by side: `BUY` or `SELL`
* `orderType?`: string — filter by order type (e.g. `LIMIT`, `MARKET`, `STOP_LOSS`, `TAKE_PROFIT`, `STOP_LIMIT`, `TRAILING_STOP`)
**Returns:** `ListAppOrdersResponse`:
* `orders`: `AppOrder[]` — matching orders
* `pageSize`: number — items per page
* `nextPageToken`: string — cursor for the next page, `""` once the walk is exhausted. Each page over-fetches one row, so an empty token is definitive: follow a non-empty token, stop on an empty one
No item count: this listing spans the hot order table and the archive, so counting behind a filter would walk the application's whole lifetime prefix on every page.
List the application's users. Public.
**Parameters:**
* `params`: `ListAppUsersParams`
* `clientId`: string — the application's public client ID (required)
* `pageSize?`: number — items per page, 1 to 1000 (default 20); a larger value is rejected
* `pageToken?`: string — cursor pagination. Omitted or `""` starts the walk; otherwise pass the previous response's `nextPageToken`. There is no `page`: the SDK rejects one with a `ValidationError` before sending, and a raw REST request carrying it gets `400`. The filters must match the request that minted the token
* `isActive?`: boolean — filter by active status
* `accountType?`: string — filter by account type: `master` or `sub`
* `address?`: string — filter by wallet address (partial match, case-insensitive)
**Returns:** `ListAppUsersResponse`:
* `users`: `AppUser[]` — matching users
* `total`: number — users matching the filter, bounded by the server count ceiling
* `pageSize`: number — items per page
* `totalPages`: number — pages at this `pageSize`, bounded by the same ceiling
* `nextPageToken`: string — cursor for the next page, `""` once the walk is exhausted. Each page over-fetches one row, so an empty token is definitive: follow a non-empty token, stop on an empty one
* `totalCapped`: boolean — `true` when more than the server ceiling matched, so `total` / `totalPages` are a lower bound
Ordered by user id ascending, **not** newest-first: `created_at` is nullable on this table, so it cannot be a total cursor key, and excluding the rows without one would hide users from the listing an application reconciles against.
List ledger movements (deposits, withdrawals, trades, fees, etc.) for the application's users. Public.
**Parameters:**
* `params`: `ListAppMovementsParams`
* `clientId`: string — the application's public client ID (required)
* `pageSize?`: number — items per page, 1 to 1000 (default 20); a larger value is rejected
* `pageToken?`: string — cursor pagination. Omitted or `""` starts the walk; otherwise pass the previous response's `nextPageToken`. There is no `page`: the SDK rejects one with a `ValidationError` before sending, and a raw REST request carrying it gets `400`. The filters must match the request that minted the token
* `userId?`: string — filter by user UUID
* `transactionType?`: string — filter by transaction type (e.g. `DEPOSIT`, `WITHDRAWAL`, `TRADE`, `FEE`, `FUNDING`, `LIQUIDATION`, `INTEREST`, `REWARD`)
* `entryType?`: string — filter by entry type (e.g. `CREDIT`, `DEBIT`, `LOCK`, `UNLOCK`, `FEE`)
* `assetId?`: string — filter by asset UUID
**Returns:** `ListAppMovementsResponse`:
* `movements`: `AppMovement[]` — matching movements
* `pageSize`: number — items per page
* `nextPageToken`: string — cursor for the next page, `""` once the walk is exhausted. Each page over-fetches one row, so an empty token is definitive: follow a non-empty token, stop on an empty one
No item count: the underlying ledger is too large to count behind a filter. A movement with no recorded timestamp is not listed — the column is nullable, and a NULL has no position in the cursor's sort key.
List user balances held within the application. Public.
**Parameters:**
* `params`: `ListAppBalancesParams`
* `clientId`: string — the application's public client ID (required)
* `pageSize?`: number — items per page, 1 to 1000 (default 20); a larger value is rejected
* `pageToken?`: string — cursor pagination. Omitted or `""` starts the walk; otherwise pass the previous response's `nextPageToken`. There is no `page`: the SDK rejects one with a `ValidationError` before sending, and a raw REST request carrying it gets `400`. The filters must match the request that minted the token
* `userId?`: string — filter by user UUID
* `assetId?`: string — filter by asset UUID
**Returns:** `ListAppBalancesResponse`:
* `balances`: `AppBalance[]` — matching balances
* `total`: number — balances matching the filter, bounded by the server count ceiling
* `pageSize`: number — items per page
* `totalPages`: number — pages at this `pageSize`, bounded by the same ceiling
* `nextPageToken`: string — cursor for the next page, `""` once the walk is exhausted. Each page over-fetches one row, so an empty token is definitive: follow a non-empty token, stop on an empty one
* `totalCapped`: boolean — `true` when more than the server ceiling matched, so `total` / `totalPages` are a lower bound
Ordered by balance id ascending. **Not** by `updatedAt`, which the removed page-number mode sorted on: that column changes on every balance movement, so a walk over it could repeat or skip rows with no concurrent insert at all.
Fetch aggregate volume and fee metrics for an application. Public.
Stats are scoped to trades where this application's users were the taker.
**Parameters:**
* `params`: `GetAppStatsParams`
* `clientId`: string — the application's public client ID (required)
* `since?`: string — ISO 8601 datetime, e.g. `"2026-01-01T00:00:00Z"`. Omit for all-time
**Returns:** `GetAppStatsResponse`:
* `volume`: string — total quote volume (normalized, human-readable)
* `makerFee`: string — total maker fees collected
* `takerFee`: string — total taker fees collected
* `applicationTakerFee`: string — application revenue share from taker fees
* `tradeCount`: number — total number of trades
**Example:**
```typescript theme={null}
const stats = await sdk.applications.getApplicationStats({
clientId: "6fe0da813f1a4adcbe675d52ca530a43",
since: "2026-05-01T00:00:00Z",
});
console.log(`Volume: ${stats.volume}, App fee: ${stats.applicationTakerFee}`);
```
# Authentication
Source: https://docs.0xmonaco.com/sdk/typescript/auth
Wallet-based authentication with ed25519 session keys
```typescript theme={null}
import { MonacoSDK } from "@0xmonaco/core";
// Initialize SDK with wallet client (see Initialization guide)
const sdk = new MonacoSDK({
walletClient,
network: "staging",
seiRpcUrl: "https://evm-rpc-testnet.sei-apis.com", // public testnet RPC — use a private provider in production
});
// Frontend authentication (challenge-response flow)
await sdk.login(clientId); // generates a session key, prompts the wallet once
await sdk.login(clientId, options); // with optional configuration
await sdk.logout(); // revoke session + clear state
await sdk.refreshAuth(); // extend the current session's expiry
// Session management (low-level, on sdk.auth)
await sdk.auth.refreshSession();
await sdk.auth.revokeSession();
// State checks
sdk.isAuthenticated();
sdk.getAuthState();
sdk.setAuthState(authState);
```
Authentication is wallet-authorized session-key signing: the wallet signs once to authorize an ed25519 session key, and the SDK then signs every request with that key. There are no access or refresh tokens.
## Frontend Authentication
Complete authentication flow: generates a session keypair, creates a challenge that commits to the session public key, signs it with the wallet, verifies it, and wires the session into every SDK module.
**Parameters:**
* `clientId` (required): Your application's public client ID
* `options` (optional):
* `connectWebSocket`: Auto-connect authenticated WebSocket channels after login (default: `false`). Public channels (OHLCV, Orderbook) never require authentication.
**Returns:** an `AuthState`:
* `sessionPublicKey`: 64-char hex ed25519 session public key registered with the server
* `sessionPrivateKey`: 64-char hex ed25519 session private key used to sign requests — **the credential; keep it on the client**
* `expiresAt`: Unix timestamp (seconds) when the session expires
* `user`: user object with `id`, `address`, `username`
* `isNewUser` (optional): `true` when this sign-in created the user — the wallet's first sign-in to this application. Gate first-time UX on it (e.g. show an optional "have a referral code?" input only to new users).
* `referralApplied` (optional): `true` when a TraderCodes referral was recorded on this sign-in (a valid TraderCode was applied). Use it to confirm the referral landed. See [TraderCodes](/developers/tradercodes).
After `login()`, all REST and WebSocket calls are signed automatically — there is no token to pass.
```typescript theme={null}
// Auto-connect authenticated WebSocket channels
const authState = await sdk.login(clientId, { connectWebSocket: true });
const pair = await sdk.market.getTradingPairBySymbol("BTC/USDC");
const unsubscribe = sdk.ws.orders(pair.id, "SPOT", callback);
// Or connect the WebSocket later
const authState = await sdk.login(clientId);
await sdk.ws.connect();
// Log out: revokes the session and disconnects authenticated sockets
await sdk.logout();
```
Revoke the current session on the server, disconnect authenticated WebSocket channels, and clear local auth state.
Extend the current session's expiry by signing a refresh request with the active session key. The keypair is unchanged; the returned `AuthState` has the updated `expiresAt`. Throws if there is no active session or it has expired/been revoked.
## Request Signing Model
The SDK signs authenticated requests automatically. Monaco accepts two signing protocols over the same session key, and `SDKConfig.requestSigning` selects which one the SDK produces — `"legacy"` by default, or `"rfc9421"`. Never send both credential sets on one request: `Signature` or `Signature-Input` alongside any `X-Monaco-*` header is rejected with `401`. `Content-Digest` is not a credential — it is ordinary integrity metadata, so it may accompany a legacy-signed request without selecting RFC verification.
### Legacy signing (default)
Direct REST clients send three headers:
| Header | Value |
| - | - |
| `X-Monaco-PublicKey` | 64-character lowercase-hex ed25519 session public key |
| `X-Monaco-Timestamp` | Unix timestamp in milliseconds, within 30 seconds of server time |
| `X-Monaco-Signature` | Hex ed25519 signature over the signing string |
The signing string is:
```text theme={null}
METHOD\npath?query\ntimestamp_ms\nSHA256_hex(body)
```
### RFC 9421 signing (opt-in)
Set `requestSigning: "rfc9421"` when constructing the SDK and it sends `Signature-Input`, `Signature`, and `Content-Digest` instead, covering `@method`, `@path`, `@query`, and the body digest. There is no automatic downgrade, so opt in only against a gateway that verifies it. See [HTTP message signatures](/reference/http-message-signatures) for the full profile and the migration sequence.
Either way the body hash is signed, so a captured request cannot be reused with a modified body, and the timestamp bounds how long it stays valid at all. Freshness is an anti-stale control, not an anti-replay one: an identical signed request can still be accepted again inside the 30-second window, and Monaco keeps no nonce or replay cache. Where an endpoint takes an `idempotencyKey` — order creation and batch creation do — treat it as required rather than optional on any retry, and reuse the original key and payload. An endpoint without one carries no exactly-once guarantee. Use the SDK unless you need a custom REST client.
## Low-Level Operations
These are the building blocks `login()` runs internally. Most applications should call `login()` and never touch them directly.
Create a single-use challenge whose message embeds the supplied session public key.
**Returns:** `nonce`, `message` (to sign with the wallet), `expiresAt`.
The challenge is single-use and authorizes exactly one keypair, once. It lives 5 minutes, or 30 minutes on a deployment where contract-signature login is enabled — a multisig has to collect owner approvals before it can answer. That window is uniform for every address rather than eligibility-specific, so this unauthenticated endpoint cannot be used to probe which addresses hold a BuilderCodes payout bucket. Read `expiresAt` rather than assuming either value.
Shares one per-client-address rate budget with `verifySignature` — 60 requests/min sustained, burst 120 by **default**, both overridable per deployment via `AUTH_RATE_LIMIT_PER_MINUTE` and `AUTH_RATE_LIMIT_BURST` — so a login costs two. Pace against the environment's own budget rather than these figures. **Enforcement is itself gated**: until `auth_rate_limit_enforced` is turned on for the deployment — it defaults off — an over-budget request is admitted with a warn log rather than rejected, so do not treat the absence of a `429` as proof you are inside the budget. Over budget is `429` with `details.retryAfter`; see [rate limits](/developers/rate-limits#authentication).
**Other outcomes:** `400` for invalid request parameters, `403` when the origin is not allowed for this application, `404` for an invalid `clientId`, and `500` for an internal failure.
Ask the connected wallet to authorize a challenge message. Returns a hex signature. An EOA signs with EIP-191 `personal_sign`; 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. Submit the returned value unchanged, and do not assume the 65-byte EOA shape — a contract signature is usually longer. Server-side EIP-1271 verification is deliberately narrow: it is offered only to a wallet holding a BuilderCodes payout bucket, on a deployment with the contract-wallet flag on and a chain RPC endpoint configured. An ordinary smart-contract trading account is NOT covered — it still takes the recovery path and cannot authenticate.
Verify the wallet signature against the challenge and register the session public key, returning a new `AuthState`. `session` is the locally-generated `{ publicKey, privateKey }` keypair whose public key was bound in `createChallenge`.
Re-running verify with a session public key that is already registered returns `409 Conflict` ("resource already exists"). `login()` generates a fresh keypair on each call, so this only affects manual low-level flows that reuse a key — generate a new session keypair instead of retrying.
A rejected signature is `401` — including a wallet contract that rejects by reverting, which is the wallet answering "no". `signature` is bounded at 8192 decoded bytes (16386 characters with the `0x` prefix); anything longer is rejected as invalid rather than forwarded to the chain. The bound is far above any realistic owner set and EOA signatures never approach it.
A `503` is **not** a verdict on the signature. Deciding how to verify an address eligible for contract-signature login means reading the chain, and a `503` covers three 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. It is retryable, and an eligible address can see it even when it turns out to be an EOA, because the code lookup precedes the choice of verification path. Addresses that are not eligible never reach the chain.
A fourth trigger has nothing to do with contract wallets: verify reads the private-beta feature-flag state before anything else, and a read that cannot be served surfaces as the same retryable `503`. **Any** address can see that one, ahead of any signature or eligibility work — so do not diagnose every `503` here as a chain or verifier problem.
Shares the authentication rate budget with `createChallenge`; over budget is `429`, checked **before** the signature, so a throttled request is never an authentication failure.
**Other outcomes:** `400` for invalid request parameters, or a nonce that has expired or already been used; `403` when the origin is not allowed for this application; `404` for an invalid nonce or `clientId`; and `500` for an internal failure.
Extend the active session's expiry (signed with the session key). Returns the new `expiresAt`. `sdk.refreshAuth()` wraps this and also updates the local `AuthState`.
Revoke the active session on the server (signed with the session key). `sdk.logout()` calls this for you.
## State Checks
Whether a session is currently active.
The current `AuthState`, or `undefined` if not authenticated.
Set the auth state directly and propagate the session key into every API module. Use it to restore a persisted session (avoiding another wallet prompt) or to share a session across SDK instances. Does not connect the WebSocket or make any API call.
```typescript theme={null}
// Restore a persisted session
const saved = JSON.parse(localStorage.getItem('monaco_session'));
if (saved && saved.expiresAt * 1000 > Date.now()) {
sdk.setAuthState(saved);
} else {
await sdk.login(clientId);
}
```
The `sessionPrivateKey` inside `AuthState` is the credential. Storing it in `localStorage` exposes it to XSS. Prefer the most secure storage your platform offers, scope it to your origin with a Content Security Policy, and clear it on logout. For React, the [`useAuth`](/sdk/react/hooks/use-auth) hook manages persistence for you.
# BuilderCodes
Source: https://docs.0xmonaco.com/sdk/typescript/buildercodes
Inspect and claim a frontend's accrued per-trade revenue share
```typescript theme={null}
import { MonacoSDK } from "@0xmonaco/core";
const config = await sdk.buildercodes.getConfig();
await sdk.buildercodes.setBuilderFee({ bps: 10 });
const feeChanges = await sdk.buildercodes.listBuilderFeeChanges({ pageSize: 20 });
const history = await sdk.buildercodes.listPayouts({ pageSize: 20 });
const summary = await sdk.buildercodes.getPayoutSummary();
const rewards = await sdk.buildercodes.getRewardsBalance();
// Claim accrued BuilderCodes payout balance into a trading balance
await sdk.buildercodes.claimBuildercodeRewards({ token, amount });
```
BuilderCodes provides per-trade frontend payouts. An enrolled frontend can earn
Monaco revenue share and can charge its own builder fee. Every positive builder
fee charged to a taker is credited in full to the enrolled application's
claimable payout balance with kind `BUILDER_FEE`; revenue-share credits use
`REVENUE_SHARE`. Claiming moves the reserved balance into a spendable trading
balance.
A claim is a ledger-only move: it debits the payout bucket and credits the
payout wallet's trading balance under the authenticated application. There is no
on-chain transaction and no gas, and the credited funds are immediately
tradeable. To take the money off the exchange, claim it and then use the
ordinary [withdrawal flow](/sdk/typescript/vault).
For taker-side attribution, how the revenue share is reported, and
payout-address rotation behavior, see the
[BuilderCodes developer guide](/developers/buildercodes).
***
All BuilderCodes methods require an authenticated, non-delegated session. No
program switch gates reads or claims, so a balance already credited stays
claimable; `setBuilderFee` needs the revenue share or builder fees switched
on. Application-level reads derive
the application from the session and require the authenticated wallet to match
its configured payout address; no method accepts a caller-selected application
or payout wallet.
Read the authenticated frontend's current BuilderCodes state.
**Returns:**
* `applicationId`: string — authenticated application UUID
* `enrolled`: boolean — whether the application is currently enrolled
* `payoutAddress?`: string | null — the configured payout wallet. A string is the address: use it. `null` and `undefined` are the two spellings of "none configured" and are the pair to normalize together — never fold a returned string in with them. In practice a successful response always carries an address, because this endpoint authorizes the caller *as* that wallet and refuses with `403` when none is set; the type admits the absent forms so a nullable wire shape cannot throw
* `buildercodesEnabled`: boolean — whether the revenue share is on. This read is not gated by it, so a successful response can report `false`
* `buildercodesBudgetBps`: number — the configured **target** share of Monaco's protocol fee on each taker fill, in basis points from `0` to `10000`. It is what the application is configured to earn, not what any given trade paid out — a payout row's `appliedBps` is the realized rate for that fill and may be lower
* `builderFeeEnabled`: boolean — whether stored builder fees are currently active
* `builderFeeBps`: number — the stored builder fee in basis points, including while builder fees are disabled
* `builderFeeCapBps`: number — the current maximum accepted fee, never above `100`
* `changesRemaining`: number — `BUILDER` changes still available in the rolling 24-hour window, from `0` to `2`
* `nextChangeAllowedAt?`: string | null — RFC 3339 time for the next allowed change when none remain
**Throws:** `401` (not authenticated), `403` (a delegated session, or a wallet that is not the application's payout address), `404` (application not found), `429` (enforced read budget exhausted), or `500` (internal error).
Set the authenticated frontend's builder fee. `bps` is required, an integer from `0` through `100`; a real change must also be at or below the current `builderFeeCapBps`. Explicit `0` clears the fee.
The application must be enrolled with a payout address. At most two frontend changes are allowed in a rolling 24 hours; Monaco administrator changes do not count. Setting the current value succeeds without a log row, email or consumed change, even when `changesRemaining` is `0` or the live cap is below that stored value. A value may be stored while `builderFeeEnabled` is `false` as long as the revenue share is on; the call is refused only while both are off. Charging is capped at the live cap and requires enrollment. A newly stored fee reaches order requests within seconds. Sequencer ordering is authoritative for the builder-fee gate and enrollment: an order processed before one of those sequenced changes uses the earlier state, and a later order uses the new state. Builder fees are charged on taker fills only, so a resting remainder that later fills as maker pays none. Each positive charged fee is credited to the application's payout balance as a `BUILDER_FEE` payout.
**Returns:** `builderFeeBps`, `previousBuilderFeeBps`, `changesRemaining`, and nullable/absent `nextChangeAllowedAt`.
**Throws:** `400` for missing/invalid `bps` or a real change above the cap; `401` without valid authentication; `403` when neither the revenue share nor builder fees are switched on, the session is delegated, or the caller is not the application's payout wallet (including when none is configured); `404` when the application is absent; `409` when enrollment is incomplete after ownership passes or the change limit is exhausted; `500` on an internal failure. A limit response carries `details.reason: "builder_fee_change_limit"` and `details.nextChangeAllowedAt`. Server keys are not accepted. A maintenance block returns `503` before the write.
List builder-fee changes newest first. `pageSize?` defaults to `20` and is at most `100`; pass an opaque `pageToken?` from the prior `nextPageToken` to continue.
**Returns:** `changes`, each with `actorType` (`"BUILDER"` or `"ADMIN"`), `actor`, `oldBps`, `newBps`, and RFC 3339 `createdAt`, plus `nextPageToken` (empty at the end). For `BUILDER`, `actor` is the payout wallet. For `ADMIN`, it is the Monaco operator's email address.
**Throws:** `400` for invalid pagination; `401` without valid authentication; `403` for a delegated session or a caller that is not the application's payout wallet; `404` when the application is absent; `429` when the enforced read budget is exhausted; `500` on an internal failure. Server keys are not accepted.
List the authenticated application's per-trade credits, newest first across persistor batches — see the ordering note below for what that does and does not guarantee inside one batch.
**Parameters:** `pageSize?` defaults to `20` and is at most `1000`, and the server rejects a value out of range with `400` rather than clamping it. `pageToken?` is an opaque cursor: omit it (or send `""`) to start from the newest credit, then pass the prior response's `nextPageToken` to continue. Page-number pagination was removed: there is no `page`, the SDK rejects a leftover one before a request is sent, and the server answers `400`. `kind?` is `"REVENUE_SHARE"` or `"BUILDER_FEE"` and filters the list; omit it for every credit. The SDK rejects any other value before a request is sent, and the server returns `400` for one.
**Ordering change.** The cursor walks `(createdAt, id)` descending. That key is total and stable — a page can neither skip nor duplicate a credit — but it is **not chronological inside one persistor batch**: `createdAt` defaults to the transaction-start `NOW()`, so every credit one batch writes shares a timestamp, and the `id` tie-break is a random UUID. Within a batch the order is therefore arbitrary, and a later fill can appear ahead of an earlier one. The removed page-number order led with the durable-log `sequence` and its ordinal precisely because those carry the engine's fill order, and one entry routinely holds several credits (a taker sweeping several makers, or a flattened batch create). Across batches the order is still newest-first.
**Throws:** `400` (invalid pagination or `kind`), `401` (not authenticated), `403` (a delegated session, or a wallet that is not the application's payout address), `404` (application not found), `429` (read budget exhausted), or `500` (internal error).
**Returns:** `payouts`, the opaque `nextPageToken` (empty once the walk is exhausted — it is one ordered read, so a short page is definitive), plus numeric `total`, `pageSize` and `totalPages` fields; with `kind`, the counts cover that kind only. Each payout contains `tradeId`, the `payoutAddress` at credit time, `token`, RAW atomic-unit `amount`, `appliedBps`, RFC 3339 `createdAt`, and `kind`. For `BUILDER_FEE`, `appliedBps` is the normalized builder-fee rate charged to the taker. For `REVENUE_SHARE`, it is the realized truncated whole-bps rate for that fill and may be lower than `buildercodesBudgetBps`.
Summarize earnings without combining unlike tokens.
**Returns:** numeric `totalPayouts`, nullable/absent `lastCreditedAt`, `byToken`, and `byTokenAndKind`. Each `byToken` total contains `token`, RAW atomic-unit `totalAmount`, numeric `payoutCount`, and RFC 3339 `lastCreditedAt`, summed across both payout kinds. `byTokenAndKind` splits the same totals by `kind` (`REVENUE_SHARE` or `BUILDER_FEE`), one row per token and kind with at least one credit; a token's rows add up to its `byToken` total. With no payouts, `totalPayouts` is `0`, both lists are empty, and `lastCreditedAt` is null/absent.
Read the authenticated wallet's reserved BuilderCodes balances. The bucket is derived server-side from that wallet; current enrollment is not required, so a former payout wallet can still inspect and claim what it accrued.
**Returns:** `balances`, one `{ token, available }` entry per token with a non-zero available RAW atomic-unit balance, or an empty array.
***
Move accrued BuilderCodes payout balance out of your reserved payout bucket and into a
trading balance.
Requires authentication. The payout bucket is resolved server-side from the authenticated
caller's own wallet address — it is never taken from the request body — so a caller can
only ever claim their own accrued balance.
**Parameters:** `ClaimBuildercodeRewardsParams`:
* `token`: string — payout token contract address (0x-prefixed). BuilderCodes shares accrue in the quote token of the traded pair.
* `amount`: string — amount to claim, in RAW atomic units of the token. Must be a positive integer and no greater than the accrued balance; an over-claim is rejected rather than partially filled.
**Returns:** `ClaimBuildercodeRewardsResponse`:
* `payoutBalance`: string — your payout-bucket balance after the claim, RAW atomic units
* `tradingBalance`: string — your trading balance for the token after the claim, RAW atomic units
**Throws:**
* `403` when the session is a delegated agent — a permanently unsupported session type for claims, not a rollout state.
* `409` when the payout balance is insufficient for the requested amount — including when the wallet has never accrued a share
* `429` when the per-user [movement budget](/developers/rate-limits) the claim shares with withdrawals and collateral transfers is enforced and spent. It is refused before the claim reaches the sequencer, so nothing committed and it is safe to retry after `retryAfter`
* `503` when the matching engine is unreachable, restarting, or slow to answer — and, more broadly, **any** untagged `5xx` or a transport failure that returned no response at all. The outcome is **unknown**, not a rejection — the claim is admitted before its reply is sent, so it may still commit, and with no idempotency key a blind retry can claim twice. Do **not** retry automatically, and do not settle it with a balance read — an admitted claim may still be queued, so an unchanged balance does not prove it will not commit. Treat the claim as pending and reconcile before issuing another. The SDK backs that up: it clears `retryable` on every failure whose outcome it cannot decide — any status at or above `500`, and a failure with no status at all because the response was never seen — against its usual "every 5xx is retryable" rule, so retry logic driven off that flag cannot claim twice.
* Two exceptions stay retryable, because both are refused before the claim can be admitted and so committed nothing: a maintenance block (`error: "OPERATIONS_BLOCKED"`) and a sequencer overload (`code: "OVERLOADED"`). An overload is the one 503 that positively wants a retry with backoff. Every `4xx` also keeps its own classification, being a decided outcome.
## Who can claim
The claim is authorized for the payout wallet itself, and it moves only that
wallet's balance. Three consequences are worth knowing before you integrate:
* **The payout wallet may be an EOA or a smart-contract wallet.** An EOA answers
Monaco's signature challenge directly. A multisig such as a Safe holds no
private key and authenticates by **EIP-1271** contract signature instead, with
Monaco asking the wallet contract whether it accepts the challenge. That path
is offered only to a wallet holding a BuilderCodes payout bucket, only while
`contract_wallet_auth_enabled` is on, and only where the deployment has a
chain RPC endpoint configured; while any of those is unmet, login is EOA-only
and a contract payout wallet can call neither this endpoint nor the withdrawal
flow that follows. While that path is enabled, challenges stay valid for 30
minutes rather than 5 — for every address, not only eligible ones, so the
unauthenticated challenge response cannot reveal which addresses hold a payout
bucket — since collecting owner approvals takes longer than a single click.
* **A wallet that never earned a share has nothing to claim.** There is no
separate "not enrolled" error; an empty bucket and a bucket that never existed
both report an insufficient balance, because in both cases there is no balance
to move.
* **Payout-address changes are not retroactive, and claiming follows that.**
When a frontend changes its configured payout address, future shares accrue to
the new wallet while everything already accrued stays with the old one.
Claiming deliberately does not require *current* enrollment, so the previous
wallet can still claim what it earned while it was the payout address.
# Copy Trading
Source: https://docs.0xmonaco.com/sdk/typescript/copy-trading
Lead trader profiles, the leaderboard, and your follows
```typescript theme={null}
import { MonacoSDK } from "@0xmonaco/core";
// Public, no session needed
await sdk.copyTrading.listLeadTraders({ sort: "roi", window: "30d" });
await sdk.copyTrading.getLeadTrader(handle);
await sdk.copyTrading.listLeadTraderClosedTrades(handle, { pageSize: 50 });
// Follower
await sdk.copyTrading.upsertFollow({ leader: handle, allocation: "1000" });
await sdk.copyTrading.listMyFollows();
await sdk.copyTrading.getFollow(followId);
await sdk.copyTrading.listLeadTraderOpenPositions(handle);
await sdk.copyTrading.addFollowPositionMargin({ followId, tradingPairId, amount: "25" });
await sdk.copyTrading.stopFollow(followId);
await sdk.copyTrading.closeFollowPositions(followId);
await sdk.copyTrading.closeAllCopyPositions();
// Leader
await sdk.copyTrading.upsertLeadTrader({ shareBps: 1000 });
await sdk.copyTrading.getMyLeadTrader();
await sdk.copyTrading.listMyFollowers();
```
For concepts and a full walkthrough see [Copy Trading](/developers/copy-trading). For live follow state, subscribe to the [`copy` channel](/sdk/typescript/websockets#copy-channel).
Leaders are addressed by their custom TraderCode handle. Every param object is validated client-side before the request is sent, and a failure throws `ValidationError` with no request made. Lists are cursor paged: pass the previous response's `nextPageToken` until it comes back empty. Optional response fields the server has no value for arrive as `null`.
***
The public leaderboard, highest first. No session needed.
**Params:**
* `sort?`: `"roi"` (default) | `"pnl"` | `"followers"` | `"allocated"` | `"aum"` | `"win_rate"` (`aum` ranks by `aumEquity`, leaders without it last)
* `window?`: `"7d"` | `"30d"` (default) | `"90d"` | `"all"`
* `pageSize?`: number — 1 to 100 (default 20)
* `pageToken?`: string — a previous response's `nextPageToken`, with the SAME `sort` and `window`
**Returns:** `leaders[]` (each `{ profile, followSummary, stats }` with stats for the requested window), `nextPageToken` (empty at the end) and `asOf` (the leaderboard snapshot time).
* `profile`: `leader` (`walletAddress`, `handle`, `display`), `status`, `shareBps`, `bio`, `minAllocation`, `registeredAt`
* `followSummary`: `followerCount`, `allocated` (AUM as allocated, not marked) and `aumEquity?` (AUM marked: the live follows' copy account equity at the engine's marks; absent when it could not be read)
* `stats`: `window`, `pnl` (realized plus unrealized, net of fees and funding), `roiPct`, `maxDrawdownPct`, `winRatePct`, `closedTrades`
One leader by custom TraderCode handle: `profile`, `followSummary`, and `stats` for every window (7d, 30d, 90d, all). No session needed. 404 for an unknown or suspended leader.
A leader's reducing executions — partial and full closes, liquidations and auto-deleveraging — newest first. No session needed. `pageSize` is 1 to 100.
**Returns:** `closedTrades[]` (`tradingPairId`, `positionSide`, `action`, `size`, `exitPrice`, `entryPrice`, `realizedPnl`, `netRealizedPnl`, `realizedRoe`, `closedAt`) and `nextPageToken`.
The open positions of a leader you currently follow (`tradingPairId`, `side`, `size`, `entryPrice`, `markPrice`, `unrealizedPnl`, `leverage`, `marginMode`). Requires authentication; 403 unless you follow that leader.
Follow a leader (no `followId`) or edit one of your follows (`followId` set). A create moves `allocation` from your margin account into a new copy account: a cross risk bucket that holds this follow's collateral and positions. Requires authentication; delegated agent and sub-account sessions get 403. A create and an edit each spend the per-account [settings-write budget](/developers/rate-limits#settings-and-copy-trading-writes) (about one write per second, burst 10) that `upsertLeadTrader` and the self-trade-prevention default share; 429 with `retryAfter` past it.
**Params:**
* `followId?`: string (UUID) — set to edit
* `leader?`: string — the leader's handle; required on a create, optional on an edit
* `allocation`: string — collateral for the copy account, in quote units
* `marginModePolicy?`: `"FOLLOW"` (default) | `"CROSS"` | `"ISOLATED"`
* `sizingMode?`: `"PROPORTIONAL"` (default) | `"FIXED_MARGIN"`
* `sizingParam?`: string — margin per leader order; required with `FIXED_MARGIN`, rejected otherwise
* `leverageMode?`: `"FOLLOW"` (default) | `"FIXED"`
* `fixedLeverage?`: string — required with `FIXED`, rejected otherwise
* `slPct?`: string — copy stop-loss as a fraction of the allocation, in `(0, 1]`
* `tradingPairIds?`: string\[] — markets to copy; omit for every market the leader trades
* `copyExisting?`: boolean — on a create, also copy the leader's currently open positions (see `previewFollow`); refused on an edit
**Returns:** `followId`, `copyRiskBucketId`, `created`.
An edit **replaces** the follow's settings. Every optional setting you leave out reverts to its default, so an edit that omits `slPct` removes the stop-loss. Send the full configuration on every edit.
Stop one of your follows: the leader's fills stop being mirrored. Never refused for the state of the account or the platform. Only three things apply: ownership, the refusal of a delegated agent session, and a per-account rate limit of its own (about one stop per second, burst 20) that closes and edits never spend; 429 with `retryAfter` past it. Returns `followId`.
Close every position in one of your follows' copy accounts at market (reduce-only, immediate-or-cancel) and cancel the resting orders and conditional orders (TP/SL) on its copy risk buckets, **keeping the follow**: the leader's next trades keep mirroring. Proceeds stay in the copy account. **Not a flatten guarantee**: a thin or halted market can leave part of a position open, and the leader's next trade opens copies again; stop the follow to end the copying. Never gated by copy trading being off or an operations block, no order budget; only a delegated agent session gets 403. Both close methods share a per-account rate limit (about one close per second, burst 5; 429 with `Retry-After`). Returns `{ followId, closeOrdersPlaced, positionsNotClosed }` once the batch is durably logged; each close's fills arrive on the order, position and copy channels. 404 for a follow that is not yours; 409 for a stopped follow or one whose stop is in progress; 400 when the close was refused; 503 when it cannot run right now.
`closeFollowPositions` for every live follow of yours, one batch per follow, keeping the follows. Returns `{ follows, stoppingFollowIds, failedFollows }`: one acknowledgement per live follow whose stop is not in progress (including one with nothing to close), the follows whose stop is in progress (left to their stop), and the follows whose close could not run (`{ followId, reason }`, a fixed sentence; retry them). The others still close.
Add margin to one of your copy positions: moves `amount` from the follow's copy account into the margin of its isolated copy position on `tradingPairId`. The position's initial margin requirement rises by exactly `amount` at the current mark (its leverage drops to what that amount buys at this price), the copy account's remaining collateral falls by the same, and your margin account is never touched. It lowers the position's leverage and moves its liquidation price away; it opens no exposure, so it works while copy trading is switched off and on a `CLOSE_ONLY` follow. `POST /api/v1/copy-trading/follows/{followId}/position-margin`; the follow id is sent in the path and only `tradingPairId` and `amount` in the body.
**Params:**
* `followId`: string (UUID) — the follow
* `tradingPairId`: string (UUID) — trading pair of the copy position
* `amount`: string — margin to add, in quote units (a positive decimal), taken from the copy account
**Returns:** `followId`, `tradingPairId`, `riskBucketId` (the copy position's isolated risk bucket), `positionId`, `positionMargin` (the position's stored margin after the add, at the entry price: it rises by `amount × entry price ÷ mark`) and `copyAccountAvailable` (what the copy account can still put into this position after the add), all strings.
There is no margin reduction: a follow exits by being stopped. Requires a master account session: delegated agent and sub-account sessions get 403. 404 for a follow that is not yours; 409 while the follow is stopping or once it is stopped; 400 with code `COPY_TRADING_REJECTED` when the add is refused (no isolated copy position on the pair, the copy position is being liquidated or is in bad debt, or more than the copy account can put in; the message names the rule); 429 over the movement budget withdrawals and collateral transfers share; 503 when the matching engine is unavailable. Not idempotent: after a timeout, read the follow before retrying, or the amount may be added twice.
Your follows, newest first. `pageSize` is 1 to 1000.
**Returns:** `follows[]` and `nextPageToken`. Each `Follow` carries `followId`, `leader`, `status` (`ACTIVE`, `CLOSE_ONLY` or `STOPPED`), the sizing, leverage and margin mode settings, `allocation`, `slPct`, `copyExisting`, `tradingPairIds`, `consecutiveSkips`, `copyRiskBucketId`, `startedAt`, `stoppedAt` and `stopReason`.
One of your follows with its most recent skipped mirrors: `{ follow, recentSkips, copyAccountEquity? }`, up to 100 skips, each `{ tradingPairId, reason, requestedQuantity, skippedAt }`. `copyAccountEquity` is the copy account's equity at the engine's marks (collateral plus unrealized PnL of its positions), a balance and not trading PnL, absent when the engine cannot be read. 404 for a follow that is not yours.
What a follow with these terms would open if it set `copyExisting`, as totals only: `{ estimatedMargin, estimatedFee, copiedPositions, skippedPositions, truncated }`. `estimatedMargin` is the collateral the copies would reserve at placement and `estimatedFee` their taker fees, both in quote units rounded up to cents; the counts say how many of the leader's positions in the copied markets would and would not be copied. The preview names no market, side or reason. At most 64 positions are sized; `truncated` is `true` when the leader holds more. Takes `upsertFollow`'s terms minus `followId`, `slPct` and `copyExisting`, with `leader` required. Read-only. It refuses terms a follow would refuse (400), is refused while copy trading is off (409), and spends the order-risk preview budget (429 when enforced). Requires authentication; delegated agent and sub-account sessions get 403, and an unknown or suspended leader is 404.
The open positions of the leader one of your follows copies, in the same shape as `listLeadTraderOpenPositions`. The leader comes from the follow, so this works after the leader releases their handle. 404 for a follow that is not yours; 403 for a stopped follow.
Register as a lead trader, or edit your profile. A registration is checked against the platform's eligibility rules (account equity, trading history, closed positions, a claimed custom TraderCode handle), and a refusal is a 400 naming the rule. Each call spends the per-account [settings-write budget](/developers/rate-limits#settings-and-copy-trading-writes) that `upsertFollow` shares; 429 with `retryAfter` past it.
**Params:**
* `shareBps?`: number — your share of followers' realized profit, 0 to 10000; omitted keeps the current share
* `bio?`: string — at most 2,000 bytes; omitted clears it
* `status?`: `"ACTIVE"` (default) | `"PAUSED"`
* `minAllocation?`: string — minimum follow allocation in quote units; omitted clears it
**Returns:** `created`, `status`, `shareBps`.
Your own lead profile, follow summary and stats. 404 when you are not a lead trader.
Your followers, as a leader: `followers[]` (`walletAddress`, `display`, `status`, `allocation`, `startedAt`) and `nextPageToken`. `pageSize` is 1 to 1000.
# Delegated Agents
Source: https://docs.0xmonaco.com/sdk/typescript/delegated-agents
Authorize a separate signer wallet to trade on behalf of an owner account
```typescript theme={null}
import { MonacoSDK } from "@0xmonaco/core";
// Owner — register and manage agents
await sdk.delegatedAgents.upsertDelegatedAgent(request);
await sdk.delegatedAgents.listDelegatedAgents();
await sdk.delegatedAgents.revokeDelegatedAgent(agentId);
// Agent — discover the owners that delegate to it, then adopt an owner-scoped session
await sdk.delegatedAgents.listDelegatedOwners();
await sdk.loginAsDelegatedOwner(ownerUserId);
```
The owner retains custody of all funds, collateral movement, and withdrawal rights. The agent
may only perform the trading actions defined in its policy. For a conceptual overview see the
[Delegated Agents](/developers/delegated-agents).
***
## Owner
Create or update the policy for an agent wallet. If an agent with the same `agentAddress`
already exists for this owner, its policy is replaced in full. An owner holds at most 10
active, unexpired agents; registering another past that is rejected until one is revoked or
expires.
**Parameters:**
* `request`: `UpsertDelegatedAgentRequest`
* `agentAddress`: string — EVM wallet address of the agent
* `name?`: string — human-readable label for the agent
* `allowedActions`: `DelegatedAgentAction[]` — required; subset of `"CREATE_ORDER"`, `"CANCEL_ORDER"`, `"REPLACE_ORDER"`, `"READ"`, `"MANAGE_RISK_BUCKETS"`; any other value is rejected. Every delegated session may read the owner's data, so an empty list or `["READ"]` is a read-only key that cannot manage orders. `"MANAGE_RISK_BUCKETS"` lets the agent call `transferCollateralToRiskBucket` for a margin account listed in `allowedMarginAccountIds`
* `allowedTradingPairIds`: string\[] — required; trading pair UUIDs the agent may trade. Send `[]` to leave markets unrestricted for an account-bound agent
* `allowedMarginAccountIds?`: string\[] — margin accounts the agent is bound to. Each listed scope must match: with both listed, an order needs a listed market **and** a listed account. Omit or send `[]` for a market-only agent, which trades its markets in every margin account; both scopes empty deny all orders
* `allowedOrderTypes?`: `Array<"LIMIT" | "MARKET">` — omit or send `[]` for unrestricted order types
* `allowedTimeInForce?`: `Array<"GTC" | "IOC" | "FOK">` — omit or send `[]` for unrestricted time in force
* `maxLeverage?`: string — maximum leverage the agent may request, e.g. `"10"`
* `maxOrderNotional?`: string — per-order notional cap in quote asset units, e.g. `"100000"`
* `maxOpenOrders?`: number — unsupported legacy field; omit it. Requests setting it are rejected because this cap was never enforced
* `expiresAt?`: string — ISO 8601 expiry; ends delegated reads, trading, and session refresh. Omit for the default of 14 days from now (also on an update); at most 180 days ahead
* `ipAllowlist?`: string\[] — up to 20 IP addresses or CIDR ranges the agent may create its delegated session from. Omit or send `[]` to allow any address
**Returns:** `DelegatedAgent`:
* `id`: string — UUID, store for revocation
* `ownerUserId`: string
* `agentAddress`: string
* `name?`: string
* `isActive`: boolean
* `expiresAt?`: string — ISO 8601
* `revokedAt?`: string — ISO 8601, set once the agent is revoked
* `allowedActions`: string\[]
* `allowedTradingPairIds`: string\[]
* `allowedMarginAccountIds`: string\[]
* `allowedOrderTypes?`: string\[]
* `allowedTimeInForce?`: string\[]
* `maxLeverage?`: string
* `maxOrderNotional?`: string
* `maxOpenOrders?`: number — legacy data only; not an enforced limit
* `ipAllowlist?`: string\[] — normalized CIDR ranges (a bare address reads back as `/32` or `/128`); empty allows any address
Direct REST callers must include `allowedActions` and `allowedTradingPairIds`, even when empty. The other four arrays also accept JSON `null` over REST. TypeScript callers should omit optional arrays or send `[]`.
**Example:**
```typescript theme={null}
const agent = await sdk.delegatedAgents.upsertDelegatedAgent({
agentAddress: "0xYourAgentWalletAddress",
allowedActions: ["CREATE_ORDER", "CANCEL_ORDER", "REPLACE_ORDER"],
allowedTradingPairIds: [btcPerpTradingPairId],
allowedOrderTypes: ["LIMIT"],
allowedTimeInForce: ["GTC", "IOC"],
maxLeverage: "10",
maxOrderNotional: "100000",
expiresAt: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString(),
ipAllowlist: ["203.0.113.0/24"],
});
console.log(agent.id); // store this for revocation
```
Return all delegated agents registered by the authenticated owner.
**Returns:** `ListDelegatedAgentsResponse`:
* `agents`: `DelegatedAgent[]`
**Example:**
```typescript theme={null}
const { agents } = await sdk.delegatedAgents.listDelegatedAgents();
const active = agents.filter((a) => a.isActive);
```
Revoke an agent policy and its existing delegated sessions. Subsequent signed requests,
including reads and session refresh, fail authentication. Connected WebSockets lose access
at their next periodic session revalidation (currently every 60 seconds). Orders already
submitted are unaffected; arrange owner-side cancellation if needed.
**Parameters:**
* `agentId`: string — the `id` returned by `upsertDelegatedAgent`
**Returns:**
* `status`: `"REVOKED"`
**Example:**
```typescript theme={null}
await sdk.delegatedAgents.revokeDelegatedAgent(agent.id);
```
***
## Agent
Reverse lookup keyed on the authenticated agent's own wallet address. Returns the owners that
currently have an active delegation to this agent — non-revoked and non-expired only. Use this
to discover the `ownerUserId` to pass into `loginAsDelegatedOwner`, so the agent does not
need the owner's ID supplied out of band.
**Returns:** `ListDelegatedOwnersResponse`:
* `owners`: `DelegatedAgentOwner[]`:
* `ownerUserId`: string — pass this into `loginAsDelegatedOwner`
* `delegationId`: string — UUID of the delegation record
* `name?`: string — the label the owner gave this agent
* `isActive`: boolean
* `expiresAt?`: string — ISO 8601 expiry of the delegation
**Example:**
```typescript theme={null}
// The agent authenticates with its own wallet first (session-key login)
await sdk.login(agentClientId);
const { owners } = await sdk.delegatedAgents.listDelegatedOwners();
const ownerUserId = owners[0].ownerUserId;
```
Adopt an owner-scoped delegated session and route all subsequent SDK calls through it. This is
the method agents should use to act on an owner — it generates a fresh ed25519 session keypair,
registers it with `createDelegatedSession`, and installs the resulting owner-scoped session as
the SDK's active auth state. Every read and trading call after it operates on the owner account,
with the agent wallet address preserved as the actor for policy enforcement and audit.
The agent must already be authenticated with its own wallet (`sdk.login`). The owner keeps
custody, collateral, positions, PnL, risk, and withdrawal control. The delegated session cannot
withdraw, and trading mutations are re-checked against the active policy every time.
**Parameters:**
* `ownerUserId`: string — the `ownerUserId` of the account owner who registered this agent,
as returned by `listDelegatedOwners`
**Returns:** `AuthState` — the adopted owner-scoped session:
* `user.id`: string — the owner's user ID (the identity subsequent calls act on)
* `user.address`: string — the agent's wallet address (the preserved actor)
* `expiresAt`: number — Unix timestamp (seconds), no later than the grant's expiry. Revocation or an earlier policy expiry ends access sooner
* `sessionPublicKey` / `sessionPrivateKey`: the freshly generated owner-scoped session keypair
**Example:**
```typescript theme={null}
// 1. The agent authenticates with its own wallet (session-key login)
await sdk.login(agentClientId);
// 2. Discover an owner that delegates to this agent
const { owners } = await sdk.delegatedAgents.listDelegatedOwners();
const ownerUserId = owners[0].ownerUserId;
// 3. Adopt the owner-scoped session — subsequent calls act on the owner
const auth = await sdk.loginAsDelegatedOwner(ownerUserId);
// auth.user.id is the owner; auth.user.address is the agent (the audit actor);
// the session expires at auth.expiresAt; earlier revocation also ends access.
// 4. Place orders against the owner's perps accounts — signed with the owner session
await sdk.trading.placeLimitOrder(btcPerpTradingPairId, "BUY", "0.5", "65000", {
tradingMode: "MARGIN",
leverage: "5",
strategyKey: "btc-mm-a",
});
```
The `strategyKey` field causes Monaco to resolve the owner's parent perps account and
target the isolated bucket for the `(tradingPair, strategyKey)` combination. Omit it to use the
owner's default bucket for the pair. See [`placeLimitOrder`](/sdk/typescript/trades)
for full order placement options.
Low-level primitive that exchanges the agent's own authenticated session for an owner-scoped
delegated session record. Prefer `loginAsDelegatedOwner` (above), which calls
this and then installs the resulting keypair as the SDK's active session. Calling
`createDelegatedSession` on its own registers a session but does **not** switch the SDK to it, so
subsequent calls keep signing with the agent's own session and act on the agent account, not the
owner.
The agent must first authenticate with its own wallet via the standard session-key login. When
the delegation has an `ipAllowlist`, the call must come from an address inside one of its
ranges. A delegated session acts against the owner account while preserving the agent wallet address as the
actor. The owner keeps custody, collateral, positions, PnL, risk, and withdrawal control. The
delegated session cannot withdraw, and trading mutations are checked against the active policy
every time they are submitted.
**Parameters:**
* `request`: `CreateDelegatedSessionRequest`
* `ownerUserId`: string — the `ownerUserId` of the account owner who registered this agent,
as returned by `listDelegatedOwners`
* `sessionPublicKey`: string — the lowercase-hex (64-char) ed25519 session public key the
agent generates for this delegated session; subsequent requests acting on the owner's
behalf are signed with the matching private key
**Returns:** `CreateDelegatedSessionResponse`:
* `expiresAt`: number — Unix timestamp when the delegated session expires, bounded by the grant's expiry
* `delegationId`: string — UUID of the active delegation record
* `ownerUserId`: string — the owner's user ID
* `agentAddress`: string — the agent's wallet address
***
## Policy Enforcement
Delegated sessions require an active, unexpired grant for every signed request and periodic
WebSocket session revalidation. Trading actions also reload and enforce the detailed policy
before create, cancel, replace, and close-position actions. Trading scopes do not filter owner
account reads while the grant remains active.
`sdk.refreshAuth()` refreshes a still-valid delegated session without changing its keypair,
but cannot extend it beyond the grant expiry. It cannot revive an expired session or renew
the owner's authorization. Keep the agent's ordinary login separate from the adopted
delegated session. To replace an expired session, use that agent login to create a fresh
delegated session; an expired grant first needs owner reauthorization.
Expiry is not permanent key revocation. If the owner renews a grant, a session whose own
expiry has not passed may become usable again. Successful `revokeDelegatedAgent()` also
revokes existing session rows, and renewing the grant does not revive those revoked sessions.
Close-position calls are checked like order creation because they submit a close or reduce order.
For delegated batch close-all flows, scope the request to an explicit trading pair so the policy
check cannot accidentally cover every open market.
## Errors
* `400` — a required JSON array is missing, a policy value is invalid (including an unknown action,
an `expiresAt` more than 180 days ahead, or an `ipAllowlist` with more than 20 entries or an entry
that is not an IP address or CIDR range), or unsupported `maxOpenOrders` is set
* `401` — a delegated session is expired/revoked, or its backing grant is missing, inactive,
expired, or revoked; applies to account reads, refresh, and signed trading requests
* `403` — agent wallet is not registered to the given owner, the policy is expired, or the
requested action is outside the agent's `allowedActions`
* `403` — `createDelegatedSession` called from a non-agent wallet (the agent must authenticate
with its own address first), or from an address outside the delegation's `ipAllowlist`
* `409` — `createDelegatedSession` called with a `sessionPublicKey` that is already registered
(for example, reused from a prior session). Generate a fresh ed25519 session keypair per session
* `404` — `revokeDelegatedAgent` called with an `agentId` that does not belong to the
authenticated owner
# Error Handling
Source: https://docs.0xmonaco.com/sdk/typescript/error-handling
Comprehensive error handling guide for Monaco Protocol SDK
The Monaco SDK provides robust error handling with typed error classes, retry logic, and helpful suggestions for common issues.
## Error Classes
All SDK errors extend the base `MonacoCoreError` class and include standardized properties for handling errors gracefully.
### Base Error Properties
```typescript theme={null}
import { MonacoCoreError } from "@0xmonaco/core";
try {
await sdk.vault.deposit(assetId, amount);
} catch (error) {
if (error instanceof MonacoCoreError) {
console.error('Code:', error.code); // Error code (e.g., "INVALID_CONFIG")
console.error('Message:', error.message); // Human-readable error message
console.error('Suggestion:', error.suggestion); // Helpful fix suggestion
console.error('Retryable:', error.retryable); // Whether retry might succeed
console.error('Timestamp:', error.timestamp); // When error occurred
}
}
```
**Common Properties:**
* `code`: MonacoErrorCode - Standardized error code
* `message`: string - Human-readable error description
* `suggestion?`: string - Actionable fix suggestion
* `retryable`: boolean - Whether retrying the operation might succeed
* `timestamp`: number - Unix timestamp when error occurred
* `cause?`: unknown - Original error that caused this error
***
## Error Codes
Every `MonacoCoreError` carries a `code` literal that identifies its class. Four are in use, one per exported class:
| `code` | Class |
| - | - |
| `INVALID_CONFIG` | `InvalidConfigError` |
| `INVALID_STATE` | `InvalidStateError` |
| `API_ERROR` | `APIError` |
| `CONTRACT_ERROR` | `ContractError` |
`@0xmonaco/core` exports exactly five error classes — `MonacoCoreError`, `InvalidConfigError`, `InvalidStateError`, `APIError`, and `ContractError` — and nothing else from its error module. There is no `ERROR_CODES` constant and no `OrderError`, `NetworkError`, `InvalidAuthError`, or `RateLimitError` to import: a network failure, an expired session, a rate limit, and a rejected order all surface as an `APIError`, distinguished by `statusCode` and the response envelope (see [APIError](#apierror) and [Order rejections](#order-rejections)). Client-side parameter validation is the one error that is not a `MonacoCoreError`: it is a `ValidationError` from `@0xmonaco/types` (see [ValidationError](#validationerror)).
***
## Error Types
### InvalidConfigError
Thrown when SDK is initialized or used with invalid configuration.
```typescript theme={null}
import { InvalidConfigError } from "@0xmonaco/core";
try {
await sdk.vault.deposit(assetId, amount);
} catch (error) {
if (error instanceof InvalidConfigError) {
console.error('Configuration issue:', error.field);
console.error('Suggestion:', error.suggestion);
// Common cases:
if (error.field === 'account') {
// Wallet has no account connected
alert('Please connect your wallet');
} else if (error.field === 'seiRpcUrl') {
// Invalid or missing RPC URL
console.error('Provide valid Sei RPC URL');
}
}
}
```
**Common Scenarios:**
* Missing required configuration (network, RPC URL, etc.)
* Invalid configuration values
* Wallet client without account (v0.5.6+)
***
### InvalidStateError
Thrown when a method is called before its prerequisite is in place. Today only `sdk.getAccountAddress()` throws it: `Wallet client not set` when the SDK was built without a `walletClient`, and `No account available` when the wallet client has no account. Calling an authenticated API method before `login()` is **not** an `InvalidStateError` — it is an `APIError` with `statusCode` `401` (see below).
```typescript theme={null}
import { InvalidStateError } from "@0xmonaco/core";
try {
const address = sdk.getAccountAddress();
} catch (error) {
if (error instanceof InvalidStateError) {
console.error('State issue:', error.message);
console.error('Suggestion:', error.suggestion);
// "Check that prerequisites are met before calling this method."
if (error.message === 'Wallet client not set') {
console.error('SDK initialized without wallet client');
} else if (error.message === 'No account available') {
console.error('Wallet not connected or account unavailable');
}
}
}
```
**Additional properties:** `currentState?` and `expectedState?` — when the SDK supplies an expected state, `suggestion` names it. There is no `field` on this class; that property belongs to `InvalidConfigError`.
***
### ValidationError
Thrown **before any request is sent** when parameters fail the SDK's client-side schema — a non-UUID `tradingPairId`, a non-positive `quantity` or `price`, a `LIMIT` without `price`, a `MARKET` with `timeInForce`, more than 100 batch items, or invalid pagination on a read. It is exported by `@0xmonaco/types` (not `@0xmonaco/core`), extends the plain `Error` — so it has no `code`, `suggestion`, or `retryable` — and is never worth retrying: fix the parameters.
```typescript theme={null}
import { ValidationError } from "@0xmonaco/types";
try {
await sdk.trading.placeLimitOrder(pairId, 'BUY', '-10', '100');
} catch (error) {
if (error instanceof ValidationError) {
console.error(error.message); // "Validation failed:\n - quantity: ..."
console.error(error.getErrors()); // { quantity: "..." } — field path → message, for forms
console.error(error.issues); // raw Zod issues
}
}
```
***
### APIError
Thrown when API requests fail (HTTP/REST errors).
```typescript theme={null}
import { APIError } from "@0xmonaco/core";
try {
// A read: safe to auto-retry on a retryable error. For order create/replace,
// see "Retrying order mutations safely" below — reconcile, don't blind-retry.
await sdk.trading.getPaginatedOrders({ status: 'SUBMITTED' });
} catch (error) {
if (error instanceof APIError) {
console.error('API Error:', error.statusCode);
console.error('Endpoint:', error.endpoint);
// Check if retryable (safe here because the wrapped call is a read)
if (error.retryable) {
// retryAfter is in SECONDS (from the Retry-After header); default to 5s.
console.log('Can retry after:', error.retryAfter ?? 5, 'seconds');
// Implement retry logic (multiply by 1000 for setTimeout, which is ms)
}
// Handle specific status codes
switch (error.statusCode) {
case 400:
console.error('Bad request:', error.message);
break;
case 401:
console.error('Unauthorized - please login');
await sdk.login(clientId);
break;
case 429:
console.error('Rate limited - wait before retrying');
break;
case 500:
console.error('Server error - retry may help');
break;
}
}
}
```
The `retryable` classification below is safe to auto-retry for **reads** (`getPaginatedOrders`, `getOrder`, balances, market data) and for **idempotent mutations** — cancelling an order or closing a position converges on the same end state however many times it runs, so a resubmit cannot double-apply.
**Retryable Status Codes (reads and idempotent mutations):**
* No status code (network error) - retryable
* 429 (Rate Limit) - retryable after `retryAfter` seconds
* 500+ (Server Error) - retryable
* 409 (Conflict) - retryable for reads; for a single order create/replace treat it as ambiguous (see below) — **except** `CLIENT_ORDER_ID_CONFLICT` and `IDEMPOTENCY_KEY_CONFLICT`, which definitively reject the current submit
**Non-Retryable Status Codes:**
* 400 (Bad Request) - fix request parameters
* 401 (Unauthorized) - re-authenticate. A `401` for clock skew (`Request timestamp outside allowed skew`) is handled for you: the SDK reads the server's time from each response's `Date` header, signs with the corrected time once the device clock is 5 seconds or more off, and retries a signed request once after a `401` that corrected the clock. A WebSocket `Authenticate` handshake rejected for skew reconnects and re-signs, up to 3 times, with whatever the clock has learned since — so it recovers once a REST response has calibrated the clock; an `AUTH_FAILED` frame carries no server time, so a socket-only client never calibrated by REST still ends in `onReauthenticationRequired`. A missing or malformed `Date` header (or a response the browser may have served from cache) leaves the offset unchanged, so the SDK falls back to the device clock. Browser clients need an API gateway that exposes the `Date` header over CORS; Monaco's does.
* 403 (Forbidden) - permission issue
* 404 (Not Found) - resource doesn't exist
**Order mutations — retain keys before retrying creates:**
Single create and each batch-create item accept an `idempotencyKey`. Reusing the same retained key with the same normalized payload returns the original committed result for 24 hours after acceptance. The SDK generates a key when omitted, but a second call generates a new one, so supply and persist your own key before sending when recovery must cross calls or process restarts. A raw unkeyed create, a retry with a new key, or a retry after 24 hours can submit a second order. A resting `LIMIT` can leave duplicate exposure on the book; an order that never rests — `MARKET`, `IOC`/`FOK`, or a `LIMIT` that fills on arrival — can execute again instead.
**Replace is different, and safer.** `replaceOrder` / `batchReplace` name the *original* order id, and the matching engine revalidates that original — in memory and on the book — at sequence time. Once one replacement succeeds the original is gone, so a retry is rejected with `ORDER_NOT_FOUND` instead of placing a second order. Duplicate execution is therefore not this endpoint's failure mode. It is still not safe to fire blindly: `ORDER_NOT_FOUND` is ambiguous on its own (the original may instead have filled or been cancelled), so reconcile to find the replacement's **new order id** rather than to rule out a double.
* **Definitively rejected — safe to resubmit:** `401` (auth-expired) and `429` (rate-limited) are rejected *before the order reaches the book*, so it was never applied — re-authenticate or back off, then resubmit. A create rejected with the maintenance `OPERATIONS_BLOCKED` `503` (see below) was likewise blocked, not applied.
* **Ambiguous create or batch item with a retained key — replay safely:** reuse the exact key and normalized payload within 24 hours. Do not change the key or payload. A different payload returns `IDEMPOTENCY_KEY_CONFLICT`; after 24 hours the same key may place a new order.
* **Ambiguous unkeyed/expired create or replace — reconcile first, never blind-retry:** after a network error, timeout, or `5xx`/`409` with no order in the body, the first attempt may already be live or filled. Reconcile on the order stream you subscribed to *before* sending; treat `getPaginatedOrders` / `getOrder` as a weak cross-check only, since they are replica-backed and an order rejected *before* acceptance is never written to them at all. See [Retrying order mutations safely](#retrying-order-mutations-safely).
* **Definitively rejected key reuse:** `IDEMPOTENCY_KEY_CONFLICT` means that key was already accepted with different normalized order details. The current submit was not applied; recover the original payload instead of changing the key.
* **Retryable — keyed submission storage:** two structured codes arrive as HTTP `503` / gRPC `UNAVAILABLE` on a single create, or as a per-item `results[].error.code` in a batch. They differ in whether an order may already exist, so do not treat them alike.
* `ORDER_SUBMISSION_CAPACITY_EXCEEDED` refuses a **new** key because retained submission receipts are at capacity while responses are protected by pending durability or persistence. The refusal neither accepts nor reserves the key, so **nothing was applied**. Capacity frees as protected receipts finish acknowledgement and persistence and become evictable — usually seconds, not the retention window — so retry the same key with bounded backoff rather than waiting for anything to expire. The same code refuses a new key when your own share is full — each user and application can hold at most 10,000 receipts (32 MiB of receipt payload) that are not yet evictable — while every other owner keeps placing orders; retries of an existing key and orders without a key are unaffected.
* `ORDER_SUBMISSION_UNAVAILABLE` means the original response for a key you already used cannot currently be retrieved, or keyed admission is busy. **Do not read it as a rejection:** 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 tell you the outcome.
Never mint a fresh key for an order you already submitted under one — the original may still commit, and a new key is a second independently admissible order. Existing retained receipts keep replaying while storage is full, and a previously accepted key still expires 24 hours after its original acceptance.
* **Definitively rejected, not ambiguous (only when you set a `clientOrderId`):** a `409` whose `error.code` is `CLIENT_ORDER_ID_CONFLICT` means another of your **resting** orders already holds that `clientOrderId`, so the current single create/replace was rejected — it was not applied, and this is the one `409` on a single create/replace that is *not* ambiguous. **If a create's response was lost**, that conflict additionally confirms the earlier attempt landed *and is still resting* — read the order named in the message. But its **absence proves nothing**, so do not resubmit in order to probe for it: the handle is claimed only while an order rests, so an attempt that filled on arrival, was `IOC`/`FOK`, or was a `MARKET` order raises no conflict, and the resubmission can execute again. Reconcile on the order stream first. Outside that retry (an accidental duplicate, or a replace colliding with an unrelated resting order) the code proves only that the current submit was rejected, not that any earlier one landed. In `batchCreate` / `batchReplace` the same conflict is a per-item `results[].error.code` in a `200` response, not a transport `409`. See [Client Order IDs](/developers/trade#client-order-ids).
**Maintenance mode and matching-engine availability:**
* During a scheduled maintenance window, mutating operations (new and replace orders, TP/SL, margin changes, collateral transfers, deposits, withdrawals, faucet mint, delegated-agent config, and reward transfers) are rejected with `503` and a stable `OPERATIONS_BLOCKED` error code (gRPC `UNAVAILABLE` with `x-maintenance-blocked` response metadata). Sign-in, order cancels, position closes, risk simulations, and delegated-session creation stay available. Retry once maintenance ends.
* `POST /api/v1/positions/{positionId}/close`, `POST /api/v1/positions/{positionId}/tp-sl`, `POST /api/v1/pitpass/rewards/transfer`, and the unauthenticated `GET /api/v1/orderbook/{tradingPairId}` return a retryable `503` (instead of `500`) when the matching engine is momentarily unreachable or restarting; non-transient engine faults still return `500`.
* Automatic parent margin account creation and cross risk bucket provisioning return retryable `503` / gRPC `UNAVAILABLE` when the sequencer refuses provisioning because its lane is full. This can surface while first funding, placing a margin order or simulating one; back off before retrying. A provisioning overload is an admission refusal, while a transport timeout may leave the larger operation's outcome unknown.
These availability errors surface as an `APIError` with `retryable: true` — they fall under the `500+` retryable rule above.
**Authentication availability (a different dependency):**
* `POST /api/v1/auth/verify` returns a retryable `503` that has nothing to do with the matching engine. Three causes are contract-signature (EIP-1271) verification of an eligible address that could not be completed — the chain did not answer, did not answer within the deadline, or the server was at its concurrent-verification capacity. A fourth is unrelated to contract wallets entirely: verify reads private-beta feature-flag state first, and a read that cannot be served returns the same `503` for any address, before any signature work. So do not attribute a `503` here to the chain, and do not look for the matching engine behind it.
* It is **not** a verdict on the signature: a rejected signature is `401`, and so is a wallet contract that rejects by reverting.
* This one surfaces as an `APIError` with `retryable: true` on the REST/TypeScript path. The gRPC surface (`AuthService.Verify`) is not a TypeScript `APIError` at all — it returns the status `UNAVAILABLE`, which a gRPC caller branches on directly.
**The one exception to the 5xx rule:** `sdk.buildercodes.claimBuildercodeRewards` clears `retryable` on every failure whose outcome it cannot decide, deliberately against the rule above. That is **any** status at or above `500` — not only `503` — and also a failure carrying **no status at all**, because the response was never seen and the request may well have been served. A `4xx` is a decided outcome and keeps its usual classification. The claim is admitted to the sequencer before its reply is sent and carries no idempotency key, so the outcome is **unknown** rather than rejected and a blind retry can claim twice; a balance read does not settle it, because an admitted claim may still be queued. Within that ambiguous set, the two tagged cases are the ones still safe to retry, because both are refused before admission: a maintenance block (`error: "OPERATIONS_BLOCKED"`) and a sequencer overload (`code: "OVERLOADED"`). This says nothing about the `4xx` responses, which keep the SDK's ordinary classification — the claim's documented `409`, like any `429`, is still flagged retryable. Do not narrow your own handling to `503` — a `500` or a connection reset on this call is exactly as ambiguous. See [claiming rewards](/sdk/typescript/buildercodes).
***
### ContractError
Thrown when smart contract operations fail.
```typescript theme={null}
import { ContractError } from "@0xmonaco/core";
try {
await sdk.vault.deposit(assetId, amount);
} catch (error) {
if (error instanceof ContractError) {
console.error('Contract error:', error.revertReason);
console.error('Contract:', error.contractAddress);
console.error('Transaction:', error.transactionHash);
// Common issues:
if (error.revertReason?.includes('insufficient allowance')) {
console.log('Need to approve tokens first');
await sdk.vault.approve(assetId, amount);
}
}
}
```
**Common Scenarios:**
* Insufficient token allowance
* Insufficient gas
* Contract execution reverted
* Invalid contract parameters
***
### Order rejections
There is no `OrderError` class. A rejected order create, replace, or cancel is an `APIError` whose `responseBody` carries the REST error envelope: `error` (the API error type, e.g. `OPERATIONS_BLOCKED`), `message`, an optional `code` (the stable matching-engine code, e.g. `POST_ONLY_WOULD_CROSS`, `ORDER_NOT_FOUND`, `CLIENT_ORDER_ID_CONFLICT`, `IDEMPOTENCY_KEY_CONFLICT`, `SELF_TRADE_NOT_ALLOWED`), `statusCode`, and optional `details`. In `batchCreate` / `batchReplace` a per-item rejection is not thrown at all — it arrives as `results[].error.code` in a `200` response.
```typescript theme={null}
import { APIError } from "@0xmonaco/core";
try {
await sdk.trading.cancelOrder(orderId);
} catch (error) {
if (error instanceof APIError) {
const body = (error.responseBody ?? {}) as { error?: string; code?: string; message?: string };
console.error('Order rejected:', error.statusCode, body.code ?? body.error, body.message);
if (error.statusCode === 404 || body.code === 'ORDER_NOT_FOUND') {
console.log('Order already filled or cancelled');
} else if (error.statusCode === 400 && body.message?.startsWith('Insufficient balance')) {
console.log('Insufficient balance for this order');
}
}
}
```
**Self-trade prevention adds two codes — and only one self-cross outcome is an error at all.** `SELF_TRADE_NOT_ALLOWED` fires exactly when a `FOK` order carrying `selfTradePreventionMode: "CANCEL_BOTH"` finds one of your own resting orders in its fill path: that placement is rejected and nothing fills or cancels — a `400` with `code: "SELF_TRADE_NOT_ALLOWED"` on a single create, a per-item `results[].error.code` in a `200` batch response. Every other self-cross outcome is a **cancellation, not an error**: the call succeeds and the affected orders terminate as `CANCELLED` with `terminalReason: "SELF_TRADE_PREVENTION"` (the WebSocket cancellation event also names the other side in `stpCounterpartyOrderId`), so watch order status rather than catching an exception. `INVALID_SELF_TRADE_PREVENTION_MODE` is the per-item `batchCreate` / `batchReplace` validation code for an unrecognized `selfTradePreventionMode` value; the same mistake on a single create or replace is a plain `400` whose message names the valid tokens, with no structured `code`. Mode semantics: [Self-Trade Prevention](/trading-mechanics/self-trade-prevention).
**Wallet shortfall vs. margin shortfall.** A genuine wallet shortfall — not enough
funds to place the order — surfaces as a `400` / invalid-input error whose message
begins `Insufficient balance`. A **margin** shortfall on a `MARGIN` order is a
different condition and now carries the risk engine's own wording (same `400` /
invalid-input type, only the message text differs):
* `insufficient initial margin: required=..., available=...` — the order needs more
initial margin than the risk bucket can supply.
* `insufficient free collateral: requested=..., available=...` — not enough free
collateral in the margin account for the requested amount.
Match on these two messages (not `Insufficient balance`) to detect a margin
shortfall. Here the wallet is not short — the margin is — so depositing more funds
does not resolve it. For `insufficient initial margin`, reduce the position size or
add collateral to the risk bucket; note that *lowering* leverage raises the required
initial margin (`notional / leverage`), so it makes this rejection worse, not better.
For `insufficient free collateral`, free up or add collateral in the margin account,
or reduce the requested amount.
***
## Retry Logic
SDK errors include a `retryable` property to indicate if retrying might succeed.
### Basic Retry Pattern
Wrap reads and convergent mutations (cancel, close-position) freely. Wrap an order
create or batch item only when the closure reuses one explicit `idempotencyKey` and
unchanged payload within 24 hours. Never wrap a replace or let each create attempt
generate a new key. See [Retrying order mutations safely](#retrying-order-mutations-safely).
```typescript theme={null}
async function withRetry(
operation: () => Promise,
maxRetries = 3,
baseDelay = 1000
): Promise {
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
return await operation();
} catch (error) {
if (error instanceof MonacoCoreError && error.retryable && attempt < maxRetries) {
// `retryAfter` is in SECONDS — multiply by 1000 for setTimeout (ms).
// Use `!== undefined` so a server-sent 0 (retry now) is honored rather
// than falling through. Otherwise back off exponentially with jitter.
const delayMs = error instanceof APIError && error.retryAfter !== undefined
? error.retryAfter * 1000
: baseDelay * 2 ** (attempt - 1) + Math.random() * 1000;
console.log(`Retry ${attempt}/${maxRetries} after ${Math.round(delayMs)}ms`);
await new Promise(resolve => setTimeout(resolve, delayMs));
} else {
throw error; // Not retryable or max retries exceeded
}
}
}
throw new Error('Max retries exceeded');
}
// Usage — wrap a read (or an idempotent mutation), never order create/replace:
const openOrders = await withRetry(() =>
sdk.trading.getPaginatedOrders({ status: 'SUBMITTED', tradingPairId: pairId })
);
```
### Advanced Retry with Rate Limiting
Same rule as above — wrap reads or idempotent mutations only. Do not wrap
`placeLimitOrder`, `placeMarketOrder`, or `replaceOrder`. See
[Retrying order mutations safely](#retrying-order-mutations-safely).
```typescript theme={null}
import { APIError } from "@0xmonaco/core";
async function retryWithRateLimit(
operation: () => Promise,
maxRetries = 3
): Promise {
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
return await operation();
} catch (error) {
if (error instanceof APIError) {
if (error.statusCode === 429 && attempt < maxRetries) {
// Rate limited - respect the Retry-After header. `retryAfter` is in
// SECONDS; convert to ms for setTimeout. Default to 5s when absent.
const delayMs = (error.retryAfter ?? 5) * 1000;
console.log(`Rate limited. Waiting ${delayMs}ms...`);
await new Promise(resolve => setTimeout(resolve, delayMs));
continue;
} else if (error.retryable && attempt < maxRetries) {
// Other retryable error - exponential backoff with jitter.
const delayMs = 1000 * 2 ** (attempt - 1) + Math.random() * 1000;
await new Promise(resolve => setTimeout(resolve, delayMs));
continue;
}
}
throw error;
}
}
throw new Error('Max retries exceeded');
}
```
***
## Retrying order mutations safely
Single create and every batch-create item support an idempotency key. The SDK sends
each request once and generates a key when omitted; because another SDK call generates
a new key, cross-call recovery requires an explicit key persisted before the first send.
Replay the same normalized payload with that key within 24 hours to receive the original
committed result. Never reuse it with changed details, and never retry an unresolved
submission after the window expires. Replace has no idempotency key. For an unkeyed or
expired create, or a replace, the safe pattern remains **reconcile, then decide** — and
when reconciliation is inconclusive, **do not resubmit**.
**A `2xx` does not mean the order executed.** An `IOC` or `FOK` that cannot fill — or a
`MARKET` order whose apparent liquidity is invalidated by maker-risk revalidation after
acceptance — is accepted and then rejected by the match: the call returns successfully with
`matchResult.status` `REJECTED`, and the stream emits `OrderPlaced` then `OrderRejected`.
Read `matchResult.status`; do not infer the outcome from the absence of an exception.
`clientOrderId` does not make a retry safe. Only a **resting** order holds one, so an
order that filled on arrival, or was a `MARKET`, `IOC` or `FOK`, releases the handle and a
resubmission under the same value is accepted as a new order and can execute again. A
`CLIENT_ORDER_ID_CONFLICT` is meaningful when it fires, but its absence proves nothing.
See [Client Order IDs](/developers/trade#client-order-ids).
**Subscribe before you send.** The order stream is the only surface pushed straight from
the matching engine, and it carries your `clientOrderId` even on orders that never rest.
It is not replayable: a slow-client disconnect (close code `1013`) gaps the stream and
there is no wire sequence number to backfill from, so a subscription opened *after* the
timeout cannot recover the acknowledgement you missed. Subscribe first — that is what lets
an event reach you at all. But note what this does *not* buy you: the fan-out is
non-persistent Core NATS, and the service can re-establish an ended upstream subscription
without closing your socket, so an event can be missed while your connection looks healthy.
Subscribing makes a positive answer possible; it never makes silence meaningful.
**Rank your evidence.** In descending order of strength:
1. **The order stream.** An `OrderPlaced`, fill, cancellation, or `OrderRejected` carrying
your `clientOrderId` is proof the order was accepted. Silence is **not** evidence of the
opposite, and no observable condition makes it so: the fan-out is non-persistent Core
NATS, and the WebSocket service can re-establish an ended upstream subscription without
closing your socket, so events can be missed while your connection still reads as
healthy. Subscribe before you send — that is what lets an event reach you at all — but
never convert its absence into a negative answer.
2. **Position or balance movement.** Attributable movement is positive proof something
executed, and it is independent of the order read model, so it survives replica lag.
The converse does **not** hold: unchanged state is inconclusive, not proof nothing
executed. `getPositions` only consults the live view for an `OPEN` status filter and
otherwise reads the same replica, and concurrent fills elsewhere can net a delta to
zero. Treat movement as evidence *for*, never absence as evidence *against*.
3. **`getOrder` / `getPaginatedOrders`.** Weakest. Written asynchronously and served from
a replica, so a *missing* row is not evidence the order did not land — most likely
right after a timeout, since a timeout usually means the system is busy. An order rejected *before* acceptance is never written at all; one rejected *after* acceptance is persisted with status `REJECTED`. A row that *is* present is real; an absent row is
inconclusive.
**When the evidence is inconclusive, hold.** The two mistakes are not symmetric: a
wrongful resubmit doubles live exposure, while a missed quote is recoverable by quoting
again. Prefer the missed quote.
Registering a handler is not the same as being subscribed: `sdk.ws.userOrders` only
adds a local handler — the SDK sends the `Subscribe` frame when the socket is open and
exposes no per-subscription acknowledgement, and the constructor connects
asynchronously. A subscription registered in the same breath as the place can
therefore miss the acknowledgement without any disconnect. Establish the subscription
**at startup** and confirm the connection is up (`sdk.ws.isConnected()`, or your
`onStatusChange` hook). Doing so is what lets a matching event reach you — it does not
make silence meaningful. Even a long-lived subscription that has been receiving other
events cannot turn silence into a negative answer, because the upstream fan-out can be
re-established without your socket changing state.
```typescript theme={null}
import { APIError, type MonacoSDK } from "@0xmonaco/core";
// At startup — once, not per order. Register a durable handler and route events
// to whoever is waiting on a clientOrderId.
const pending = new Map void>();
sdk.ws.userOrders((event) => {
const coid = event.data?.clientOrderId;
if (coid) pending.get(coid)?.(event.data.orderId ?? null);
});
// Wait for the connection before trading: registration alone does not prove the
// server-side subscription is active.
// Ambiguous = you never got a definitive response: network error, timeout, or a
// 5xx/409 with no order in the body. 401/429 are definitive rejections — auth is
// validated, and rate limits are applied, before the order reaches the book, so it
// was never placed and those may be resubmitted directly.
// The envelope carries TWO discriminators and they are different fields: `code` is the
// matching-engine code (CLIENT_ORDER_ID_CONFLICT), `error` is the API error type
// (OPERATIONS_BLOCKED). Read whichever the case needs.
function envelope(error: APIError): { code?: string; error?: string } {
const body = error.responseBody;
return typeof body === "object" && body !== null ? (body as { code?: string; error?: string }) : {};
}
function isAmbiguous(error: unknown): boolean {
if (!(error instanceof APIError)) return true; // no HTTP status → assume ambiguous
if (error.statusCode === 401 || error.statusCode === 429) return false;
// A 409 is ambiguous EXCEPT when the engine names a deterministic conflict:
// either code definitively rejects this submit, so there is nothing to reconcile.
if (error.statusCode === 409) {
return !["CLIENT_ORDER_ID_CONFLICT", "IDEMPOTENCY_KEY_CONFLICT"].includes(envelope(error).code ?? "");
}
// Maintenance is a deliberate pre-book rejection, not an ambiguous failure: the
// order was blocked, never applied. Retry once the window ends.
if (error.statusCode === 503 && envelope(error).error === "OPERATIONS_BLOCKED") return false;
return error.statusCode === undefined || error.statusCode >= 500;
}
async function placeOrderReconciling(
sdk: MonacoSDK,
pairId: string,
side: "BUY" | "SELL",
quantity: string,
price: string,
): Promise {
// Cheap precondition, NOT a guarantee: `isConnected()` reads local socket state
// only (`readyState === OPEN`). The SDK sends Authenticate and Subscribe on open
// and reports "connected" immediately, while the server activates the
// subscription asynchronously — so this can pass before `orders` is live. It is
// worth checking because a closed socket means no positive answer is possible at
// all; it does not establish that one would reach you.
if (!sdk.ws.isConnected()) {
throw new Error("order stream not connected — refusing to place without it");
}
const clientOrderId = `quote-${crypto.randomUUID()}`;
// Arm the matcher BEFORE sending, on the subscription established at startup,
// and keep it armed for as long as the request is in flight. The stream is not
// replayable, so a subscription opened after the failure cannot recover the
// acknowledgement. Note there is no timer here: the SDK's fetch has no deadline
// of its own, and a bounded wait started now could expire while the request is
// still pending — disarming the matcher just before the event arrives.
const landed = new Promise((resolve) => {
pending.set(clientOrderId, resolve);
});
try {
const res = await sdk.trading.placeLimitOrder(pairId, side, quantity, price, {
clientOrderId,
});
return res.orderId;
} catch (error) {
if (!isAmbiguous(error)) throw error; // definitively rejected — never applied
// Ambiguous. Only NOW start the bounded wait, so the window measures how long
// we wait for the acknowledgement rather than overlapping the request itself.
const orderId = await Promise.race([
landed,
new Promise((resolve) => setTimeout(() => resolve(null), 10_000)),
]);
if (orderId) return orderId; // it landed — do NOT resubmit
// No event. This is NOT evidence the order failed — the upstream fan-out can
// be re-established without this socket changing state, so no observable
// condition makes silence conclusive. Movement works one way only: an attributable position/balance change means
// the order DID execute, so it blocks placing again — but unchanged state is
// inconclusive and never clears you to place. If anything is unclear, hold and
// surface the ambiguity rather than risking a double.
return null;
} finally {
pending.delete(clientOrderId);
}
}
```
Reconcile for `replaceOrder` with a different question in mind. A retry cannot open a
duplicate — the original is revalidated at sequence time and a second attempt returns
`ORDER_NOT_FOUND` — so what you are resolving is *which* state you are in, not whether you
doubled. A successful replace assigns a **new** order id and marks the original
`CANCELLED` with `terminalReason: "REPLACED"`, so watch the stream for the replacement
carrying your `clientOrderId`. `getOrder(originalOrderId)` is the weaker fallback and is
asymmetric: `terminalReason: "REPLACED"` is positive evidence your replacement landed and
you need its new id, but an original that still reads live is **inconclusive** — order
persistence is asynchronous, so that row can predate a replacement the engine has already
applied. Do not treat it as proof the replace failed.
### Batch create: inspect per item, clean up failed legs
`batchCreate` returns **partial success** — a failed leg does not stop the others.
When the call *resolves*, inspect each item: a succeeded leg carries an `orderId`, while
an item that did not return success carries an `error` and an empty `orderId`. Never
resubmit the whole batch. A deterministic validation or business rejection can be
corrected and resubmitted by itself, but `SERVICE_UNAVAILABLE` has an **unknown outcome**:
reconcile that leg first and retry only with its original explicit `idempotencyKey`.
```typescript theme={null}
const legs = [
{ tradingPairId: pairId, orderType: 'LIMIT', side: 'BUY', price: '35000.00', quantity: '0.5', idempotencyKey: crypto.randomUUID() },
{ tradingPairId: pairId, orderType: 'LIMIT', side: 'SELL', price: '36000.00', quantity: '0.3', idempotencyKey: crypto.randomUUID() },
];
const batch = await sdk.trading.batchCreate(legs);
console.log(`${batch.totalSucceeded}/${batch.totalRequested} created`);
batch.results.forEach((result, index) => {
if (!result.error) return;
if (result.error.code === 'SERVICE_UNAVAILABLE') {
// Unknown outcome: reconcile first. If a retry is still needed, submit only
// legs[index] so it carries the exact same explicit idempotencyKey.
console.warn('Leg outcome unknown:', legs[index].idempotencyKey);
return;
}
console.warn('Leg rejected:', result.error.code, result.error.message);
});
// If one side landed and the other failed you are one-sided: cancel the live legs
// (cancel is idempotent — safe to retry) or place the missing side before quoting.
```
If the whole `batchCreate` call instead **throws** (network error, timeout, or a
`5xx` with no body), an unknown subset of the legs may already be live. Replay each
unresolved leg with its exact explicit `idempotencyKey` and unchanged payload within
24 hours. If a leg has no retained key or its window expired, reconcile it like a
legacy single order: use the order stream subscribed before sending, then position or
balance movement, with `getPaginatedOrders` only as a weak replica-backed cross-check.
If that evidence is inconclusive, hold rather than resubmit.
***
## Error Suggestions
Every `MonacoCoreError` carries a `suggestion` the SDK derives from the failure — the configuration field, the HTTP status and message, or the contract revert reason. `ValidationError` has none; read its `message` or `getErrors()` instead.
```typescript theme={null}
try {
const sdk = new MonacoSDK({ network: 'invalid-network', seiRpcUrl });
} catch (error) {
if (error instanceof InvalidConfigError) {
console.error('Error:', error.message);
console.error('Suggestion:', error.suggestion);
// Output:
// Error: "network must be one of: mainnet, development, staging, local. Got: invalid-network"
// Suggestion: "Check the 'network' configuration parameter. Valid networks are: 'mainnet', 'development', 'staging', 'local'."
}
}
```
**Suggestions the SDK produces:**
| Error Type | Scenario | Suggestion |
| - | - | - |
| **InvalidConfigError** | Unknown `network` | "Check the 'network' configuration parameter. Valid networks are: 'mainnet', 'development', 'staging', 'local'." |
| | Missing or malformed `seiRpcUrl` | "Check the 'seiRpcUrl' configuration parameter. Use '[https://evm-rpc-testnet.sei-apis.com](https://evm-rpc-testnet.sei-apis.com)' for testnet or '[https://evm-rpc.sei-apis.com](https://evm-rpc.sei-apis.com)' for mainnet." |
| | No wallet client, or its chain does not match the network (`field: 'walletClient'`) | "Check the 'walletClient' configuration parameter. Ensure you're creating the wallet client with a valid account and chain configuration." |
| | Wallet client has no account (`field: 'account'`) | "Check the 'account' configuration parameter. Refer to the SDK documentation for correct configuration format." |
| **InvalidStateError** | `getAccountAddress()` without a wallet client or account | "Check that prerequisites are met before calling this method." |
| **ValidationError** (`@0xmonaco/types`) | Parameters fail the client schema — negative quantity, non-UUID id, more than 100 batch items | No `suggestion`; `message` is `Validation failed:` followed by one `field: reason` line per issue, and `getErrors()` returns them keyed by field |
| **ContractError** | Deposit before approval (`revertReason: 'APPROVAL_REQUIRED'`) | No `suggestion`; the message itself says to call `approve()` first for the asset |
| | Chain revert containing `insufficient allowance` | "Token allowance is insufficient. Call sdk.vault.approve() to increase the token allowance for the vault contract." |
| | Chain revert containing `insufficient balance` | "Wallet balance is insufficient for this transaction. Ensure you have enough tokens and native currency for gas fees." |
| | Revert mentioning `nonce` | "Transaction nonce error. This may be due to a pending transaction. Wait for pending transactions to complete or reset your account nonce." |
| | Revert mentioning `gas` | "Transaction ran out of gas. Try increasing the gas limit for this transaction." |
| | Withdrawal execution failed with no revert reason | "Smart contract execution failed. Check transaction parameters and try again." |
| **APIError (no `statusCode`)** | Network failure or timeout (`Network request failed for …`) | "Network request failed. Check your internet connection and try again." — `retryable: true` |
| **APIError (4xx)** | 400 whose message contains `Insufficient balance` | "Insufficient balance. Deposit more funds using sdk.vault.deposit() or reduce the order size." |
| | 400 whose message contains `invalid` | "Invalid request parameters. Check the API documentation for correct parameter format and values." |
| | Any other 400, including the margin shortfalls above | "Bad request. Verify that all required parameters are provided and correctly formatted." |
| | 401 before `login()` (`Session keypair not set. Authenticate (login) first.`) or `refreshAuth()` without a session | "Authentication required. Call sdk.login(clientId) to authenticate before making this request." |
| | 401 whose message contains `expired` | "Access token has expired. Call sdk.refreshAuth() to get a new token, or call sdk.login() to re-authenticate." |
| | 403 Forbidden | "Access denied. Ensure your application has the required permissions and your client ID is valid." |
| | 404 Not Found | "Resource not found. Check that the ID or identifier is correct and the resource exists." |
| | 409 Conflict | "Resource conflict. The resource may have been modified. Refresh and try again." — `retryable: true`, but apply the `409` rules under [APIError](#apierror) before resubmitting an order |
| | 429 Too Many Requests | "Rate limit exceeded. Wait a moment before retrying. Consider implementing exponential backoff for retries." — sleep `error.retryAfter` seconds (parsed from `Retry-After`). When `retryAfter` is `undefined` on a `429`, the block came from the edge, which sets no `Retry-After`: read the wait from `error.responseBody.details.retryAfter` (seconds) or fall back to a bounded, jittered backoff — never retry immediately, since the block lasts minutes to an hour. `APIError` exposes no other response headers, so the `x-ratelimit-*` budget headers order endpoints return are readable only from a raw HTTP client; authenticated reads draw a separate per-account budget with the same `retryAfter` and no headers, so move polling to the WebSocket streams; see [Rate Limits](/developers/rate-limits) |
| **APIError (5xx)** | 500–599 | "Server error occurred. This is likely a temporary issue. Try again in a few moments." — `retryable: true` |
**Example: Using suggestions in UI**
```typescript theme={null}
try {
await sdk.vault.deposit(assetId, amount);
} catch (error) {
if (error instanceof MonacoCoreError && error.suggestion) {
// Show suggestion to user
toast.error(`${error.message}\n\nSuggestion: ${error.suggestion}`);
} else {
toast.error(error.message);
}
}
```
***
## Complete Error Handling Example
```typescript theme={null}
import {
MonacoSDK,
MonacoCoreError,
InvalidConfigError,
InvalidStateError,
APIError,
ContractError,
} from "@0xmonaco/core";
import { ValidationError } from "@0xmonaco/types";
async function placeOrderWithHandling(
sdk: MonacoSDK,
pairId: string,
side: 'BUY' | 'SELL',
quantity: string,
price: string,
attempt = 1
) {
// Bound the retries so persistent ambiguity/auth/rate-limit can't recurse forever.
if (attempt > 5) throw new Error('Max order-placement attempts exceeded');
try {
const order = await sdk.trading.placeLimitOrder(pairId, side, quantity, price);
console.log('Order placed:', order.orderId);
return order;
} catch (error) {
// Handle specific error types
if (error instanceof InvalidConfigError) {
console.error('Configuration error:', error.message);
if (error.suggestion) {
console.log('Fix:', error.suggestion);
}
throw error; // Can't recover from config errors
} else if (error instanceof ValidationError) {
// Rejected client-side before any request was sent — fix the parameters, do not retry.
console.error('Invalid order parameters:', error.getErrors());
throw error;
} else if (error instanceof InvalidStateError) {
console.error('State error:', error.message);
throw error;
} else if (error instanceof APIError) {
console.error('API error:', error.statusCode, error.message);
// 401 and 429 are DEFINITIVE rejections — the order never reached the book,
// so re-authenticating / backing off and resubmitting cannot double-submit.
if (error.statusCode === 401) {
console.log('Re-authenticating...');
await sdk.login('your-client-id');
return placeOrderWithHandling(sdk, pairId, side, quantity, price, attempt + 1);
} else if (error.statusCode === 429 && error.retryable) {
// `retryAfter` is in SECONDS — convert to ms for setTimeout.
const delayMs = (error.retryAfter ?? 5) * 1000;
console.log(`Rate limited. Waiting ${delayMs}ms...`);
await new Promise(resolve => setTimeout(resolve, delayMs));
return placeOrderWithHandling(sdk, pairId, side, quantity, price, attempt + 1);
} else if (error.retryable) {
// AMBIGUOUS (network / timeout / 5xx / 409 with no order): the create MAY
// already be live or filled. There is no safe automatic resubmit here.
//
// The order list CANNOT clear this: it is replica-backed, so a missing row
// is not evidence the order did not land, and an order rejected
// before acceptance is never written to it at all (one rejected after
// acceptance is persisted with status REJECTED). Resubmitting on "no match" is the double-execution
// path — see "Retrying order mutations safely" above.
//
// Resolve it on the order stream you subscribed to BEFORE sending, keyed on
// a clientOrderId. A matching event settles it; silence does NOT, and no
// observable condition makes silence meaningful. Absent positive evidence
// the outcome stays unresolved: hold and escalate rather than placing again.
console.warn('Ambiguous create — reconcile on the order stream; not resubmitting.');
throw error;
}
throw error;
} else if (error instanceof ContractError) {
console.error('Contract error:', error.revertReason);
if (error.revertReason?.includes('insufficient allowance')) {
console.log('Approving tokens...');
// Handle approval and retry
}
throw error;
} else if (error instanceof MonacoCoreError) {
// Generic MonacoCoreError handling
console.error('SDK error:', error.code, error.message);
if (error.suggestion) {
console.log('Suggestion:', error.suggestion);
}
throw error;
} else {
// Unexpected error
console.error('Unexpected error:', error);
throw error;
}
}
}
```
***
## Best Practices
1. **Always check error types** - Use `instanceof` to handle different error classes
2. **Use retry logic for retryable errors** - Check `error.retryable` property
3. **Respect rate limits** - `error.retryAfter` is in **seconds**; multiply by 1000 before passing it to `setTimeout` (which expects milliseconds)
4. **Add jitter to backoff** - When no `retryAfter` is provided, use exponential backoff with a small random jitter so concurrent clients don't retry in lockstep
5. **Show user-friendly messages** - Use `error.suggestion` for helpful guidance
6. **Log for debugging** - Include `error.code`, `timestamp`, and `cause`
7. **Handle auth errors** - Re-authenticate on 401 errors and retry
8. **Don't retry non-retryable errors** - Avoid wasting time on errors that won't succeed
9. **Retain create idempotency keys before sending** - Replay a single create or batch item only with the same explicit key and normalized payload within 24 hours. For a replace, unkeyed create, or expired key after an ambiguous outcome, [reconcile first](#retrying-order-mutations-safely) on the order WebSocket stream subscribed before sending. Only a matching event resolves it: silence is never evidence the order did not land, and replica-backed order reads are only a weak cross-check. **When the evidence is inconclusive, hold rather than resubmit.** Cancels and position closes are safe to retry.
***
## See Also
* [TypeScript SDK](/sdk/typescript/intro) - SDK setup and configuration
* [Authentication](/sdk/typescript/auth) - Login and auth error handling
* [Order Management](/developers/trade) - Order placement error handling
# TypeScript SDK
Source: https://docs.0xmonaco.com/sdk/typescript/intro
@0xmonaco/core
The Monaco SDK requires a Viem wallet client and network configuration. This page covers installation, SDK initialization, configuration options, and best practices.
## Installation
```bash npm theme={null}
npm install @0xmonaco/core @0xmonaco/types
```
```bash pnpm theme={null}
pnpm add @0xmonaco/core @0xmonaco/types
```
```bash yarn theme={null}
yarn add @0xmonaco/core @0xmonaco/types
```
## Basic Initialization
Monaco needs a Viem `walletClient` configured for Sei testnet during private alpha. You can create that client from an injected browser wallet, WalletConnect, a local private key, or any other Viem-supported wallet setup. Monaco will support mainnet wallet clients when mainnet access opens. See the [Viem Wallet Client docs](https://viem.sh/docs/clients/wallet) for the full set of options.
```typescript Browser Wallet theme={null}
import { MonacoSDK } from "@0xmonaco/core";
import { createWalletClient, custom } from "viem";
import { seiTestnet } from "viem/chains";
const walletClient = createWalletClient({
chain: seiTestnet,
transport: custom(window.ethereum!)
});
await walletClient.requestAddresses();
const sdk = new MonacoSDK({
walletClient,
network: "staging",
seiRpcUrl: "https://evm-rpc-testnet.sei-apis.com", // Required: Your Sei RPC endpoint
});
```
```typescript Local Account theme={null}
import { MonacoSDK } from "@0xmonaco/core";
import { createWalletClient, http } from "viem";
import { privateKeyToAccount } from "viem/accounts";
import { seiTestnet } from "viem/chains";
const account = privateKeyToAccount("0x...");
const walletClient = createWalletClient({
account,
chain: seiTestnet,
transport: http()
});
// Initialize SDK
const sdk = new MonacoSDK({
walletClient,
network: "staging",
seiRpcUrl: "https://evm-rpc-testnet.sei-apis.com", // Required: Your Sei RPC endpoint
});
```
## Configuration Options
Viem wallet client for signing transactions and messages. Must be configured with a valid account and chain.
**Required (v0.4.0+)**: Sei blockchain RPC endpoint URL. Each application should configure their own RPC provider to avoid rate limits. For testnet/staging, use `https://evm-rpc-testnet.sei-apis.com`. For production, use a dedicated RPC provider like Ankr, GetBlock, or AllThatNode.
For public usage, set this to `"staging"` or `"mainnet"`. Selects the chain (`"staging"` uses Sei testnet; `"mainnet"` uses Sei mainnet) and the default API and WebSocket URLs. The SDK type also supports internal presets, but public docs intentionally cover only public endpoints.
Optional. Poll interval, in milliseconds, for the SDK's internal viem public client when it waits on transaction receipts. Defaults to 750ms (viem's own default is 4000ms), so `sdk.vault.deposit` and `sdk.waitForTransaction` resolve within about one to two Sei blocks. Must be a positive finite number, otherwise the SDK throws `InvalidConfigError` on construction. Set it to `4000` to restore the previous cadence.
Optional. Which protocol signs authenticated REST requests: `"legacy"` (the default) sends the `X-Monaco-*` headers, `"rfc9421"` sends RFC 9421 HTTP message signatures. Both use the same session key, and WebSocket and gRPC authentication are unaffected either way. There is no automatic downgrade, so select `"rfc9421"` only against a gateway that verifies it. In RFC mode the SDK requires a string request body, disables redirect following on authenticated requests, and throws if the caller supplies a signature header itself. See [HTTP message signatures](/reference/http-message-signatures).
## Network Configurations
The SDK automatically configures API and WebSocket endpoints based on the network parameter:
```typescript theme={null}
const sdk = new MonacoSDK({
walletClient,
network: "staging",
seiRpcUrl: "https://evm-rpc-testnet.sei-apis.com",
});
```
* **Chain**: Sei Atlantic-2 testnet
* **Chain ID**: 1328
* **API**: Staging API Gateway
* **WebSocket**: Auto-resolved from `network`
## RPC Provider Options
`seiRpcUrl` points the SDK at Sei for on-chain reads and transaction confirmation. During private alpha, use the Sei testnet RPC. In production code, source `seiRpcUrl` from an environment variable rather than hardcoding it.
## SDK Instance Properties
Once initialized, the SDK exposes these APIs:
```typescript theme={null}
const sdk = new MonacoSDK({
walletClient,
network: "staging",
seiRpcUrl: "https://evm-rpc-testnet.sei-apis.com",
});
// API instances
sdk.auth // Authentication operations
sdk.applications // Application config
sdk.fees // Fee simulation and queries
sdk.vault // Vault operations (deposits/withdrawals)
sdk.trading // Trading operations (orders)
sdk.market // Market data queries
sdk.profile // User profile and account info
sdk.trades // Trade history queries
sdk.orderbook // Orderbook snapshots
sdk.withdrawals // Low-level withdrawal calldata (no broadcast)
sdk.subAccounts // Master-account sub-accounts
sdk.faucet // Testnet token faucet
// WebSocket client (v0.5.0+)
sdk.ws // Unified WebSocket client (auto-connects on init)
sdk.ws.orders() // Subscribe to order events (requires auth)
sdk.ws.orderbook() // Subscribe to orderbook updates
sdk.ws.ohlcv() // Subscribe to candlestick data
sdk.ws.trades() // Subscribe to trade events
sdk.ws.movements() // Subscribe to balance movements (requires auth)
// Blockchain clients
sdk.walletClient // Viem wallet client
sdk.publicClient // Viem public client
// Utility methods
sdk.isConnected() // Check if wallet is connected
sdk.isAuthenticated() // Check if user is logged in
sdk.getAccountAddress() // Get current wallet address
sdk.getNetwork() // Get current network name
sdk.getChainId() // Get current chain ID
sdk.waitForTransaction(hash) // Wait for tx confirmation
```
## SDK Utility Methods
Check if the SDK has a valid wallet connection
Check if the user has a valid authentication session
Get the current wallet address. Throws `InvalidStateError` if no account is connected.
**Error Handling:**
```typescript theme={null}
import { InvalidStateError } from "@0xmonaco/core";
try {
const address = sdk.getAccountAddress();
console.log("Wallet address:", address);
} catch (error) {
if (error instanceof InvalidStateError) {
console.error("No wallet connected");
// Prompt user to connect wallet
} else {
throw error;
}
}
```
**Best Practice:** Use `isConnected()` before calling:
```typescript theme={null}
if (sdk.isConnected()) {
const address = sdk.getAccountAddress();
} else {
console.warn("No wallet connected");
}
```
Get the current network name, for example `'staging'`
Get the current blockchain chain ID (1328 for testnet)
Wait for a transaction to be confirmed on-chain
**Parameters:**
* `hash`: Transaction hash to wait for
* `confirmations?`: Number of confirmations required (optional)
* `timeout?`: Timeout in milliseconds (optional)
**Returns:** Viem `TransactionReceipt`
# Managed Markets
Source: https://docs.0xmonaco.com/sdk/typescript/managed-markets
Claim a fenced maker lease and refresh the server-bounded quote set on an operator-managed launch
```typescript theme={null}
import { MonacoSDK } from "@0xmonaco/core";
const inventory = await sdk.managedMarkets.listAssignments({ pageSize: 100 });
// Claim a lease, then keep refreshing quotes under it
const lease = await sdk.managedMarkets.claimLease(launchId, { ownerId });
const status = await sdk.managedMarkets.refreshQuotes(launchId, {
ownerId,
generation: lease.generation,
// Derive the next revision from the lease the engine just acknowledged.
// A renewal keeps the generation's existing revision, so a hard-coded "1"
// restates the quote set once the held revision is 1 and is refused
// outright once it has advanced past it.
quoteRevision: (BigInt(lease.quoteRevision) + 1n).toString(),
});
```
An operator-managed market launch is provisioned by Monaco and quoted by a maker
wallet the operator configured for that application. This namespace is that
maker's control surface: it reports the assignments the caller is expected to
quote, hands out a short **lease** that fences one process at a time, and asks
the engine to refresh a quote set whose prices, sizes, fees and risk bucket are
all chosen server-side. It never places an order you describe and never moves
funding.
***
All three methods require an authenticated **wallet** session, and delegated-agent
sessions are refused with `403` on every one of them, permanently rather than as
a rollout state.
The maker requirement applies differently per method. `claimLease` and
`refreshQuotes` are launch-scoped and need a session whose user *and* application
both match that launch's configured maker; a launch the caller is not the maker
for reads as `404` rather than `403`, so the endpoint never confirms a launch the
caller has no part in. `listAssignments` takes any authenticated wallet and
simply filters by it — a non-maker gets an **empty page**, not an error.
`ownerId` is a **process fence, not authorization**. It is a UUID you choose to
identify the process holding the lease; authorization is derived entirely from
the authenticated session's user and application binding and never from any
identity in the request body, so no `ownerId` widens what a caller can reach.
It does not let a standby seize a live lease either. A claim from a **different**
`ownerId` succeeds only once the current lease has expired; until then it answers
`409`. So a takeover is "wait out the remaining lease, then claim", not "claim
harder". Claiming after expiry bumps `generation`, which is what makes the
previous holder's next refresh fail as stale.
A claim from the **current** owner behaves differently depending on
`expectedGeneration`. Supply it and the call **renews**: the lease's expiry moves
out and the generation is unchanged. Omit it while you still hold an unexpired
lease and the call **reconciles rather than renews** — it replays the existing
reservation and returns its original `expiresAt`. A loop that keeps omitting the
generation expecting a renewal will let its own lease lapse.
`generation` and `quoteRevision` are `uint64` fences carried as **exact decimal
strings** in JSON (`ManagedMarketFence`) and as `uint64` over gRPC. Keep the
wire value as text, but **do not order or increment it as text** — `"10" < "2"`
lexically — and do not round-trip it through `Number`, which loses values above
2^53. Use `BigInt`:
```typescript theme={null}
const isStale = BigInt(local.generation) !== BigInt(server.generation);
const nextRevision = (BigInt(current) + 1n).toString();
```
List the launches this maker wallet is assigned to, for the authenticated
application.
**Parameters:** all optional
* `pageSize?`: number — `1`–`1000`; this endpoint defaults it to `100` rather than the shared `20`. A larger value is rejected, not clamped
* `pageToken?`: string — opaque cursor: omit it (or send `""`) to start from the **oldest** assignment, then pass the prior response's `nextPageToken` to continue. The walk seeks on `(createdAt, id)` **ascending** — oldest first, the order this listing already read in — both key columns fixed for an assignment's lifetime, so a launch assigned mid-walk cannot make the walk repeat or drop a row
Page-number pagination was removed: there is no `page`, and a request still sending one is a `400`. A page size out of range is the `400` below: `pageSize must be between 1 and 1000`.
**Returns:** `ListManagedMarketAssignmentsResponse`:
* `items`: `ManagedMarketAssignment[]` — `launchId`, `tradingPairId`, `symbol`, `phase`, `refreshIntervalMs`, `allocationUsdc` and `quoteEnabled`
* `total`, `pageSize`: number
* `inventoryObservedAt`: string — when the server read this inventory (ISO 8601)
* `nextPageToken`: string — pass back as `pageToken`; empty once the walk is exhausted. This listing is one ordered read, so a page shorter than `pageSize` is a definitive end
The list is **expected inventory**, not a ready-to-quote set: a row with
`quoteEnabled: false` is still yours and still listed, but `claimLease` and
`refreshQuotes` on it answer `409` until the launch is ready to quote: its
launch phase is `"warming"`, `"activating"` or `"live"`, its runtime phase is
warming or live, and its allocated collateral has reached the configured
allocation in full. **`"activating"` counts** — keep quoting through
activation, which is the phase that depends on those quotes.
**`lease` is declared on the type but never populated by this endpoint.** The
handler sets it to `None` on every row on purpose — the database reservation
cannot prove the engine's current generation or quote revision, and a
fabricated zero would read as a usable fence — and the field is
`skip_serializing_if = "Option::is_none"`, so the **key is absent from the JSON
entirely** rather than present as `null`. `claimLease` is the only source of a
lease you may act on.
`phase` here is the **launch** phase: `"pending"`, `"validating"`,
`"registering"`, `"warming"`, `"activating"`, `"live"` or `"failed"`. The
phase on a quote refresh is a different, narrower vocabulary — see
`refreshQuotes` below.
**Errors:** `401` (auth required), `403` (delegated-agent session), `400`
(invalid pagination), `429` (this is an authenticated `GET`, so it is charged
to your per-account **read** budget before the handler runs — a polling loop
here is bounded like any other read), `500`.
A wallet that is simply not this application's configured maker is not an
error: it gets an empty page. `403` is for delegated-agent sessions only.
Claim, renew or reconcile the maker lease on one launch. A lease runs for 30
seconds and is the fence every quote refresh is checked against.
**Parameters:**
* `ownerId`: string — UUID identifying the claiming process
* `expectedGeneration?`: `ManagedMarketFence` — **required when renewing** an unexpired lease you already hold; omit it for an initial claim or after expiry
**Returns:** `ManagedMarketLease` — `launchId`, `ownerId`, the engine-observed
`generation` and `quoteRevision`, `expiresAt`, `refreshIntervalMs` and the
launch `phase`.
The reply is the **engine's** view, not the database reservation: the call
reserves the lease, asks the matching engine to fence it, and then rechecks
ownership, so a lease that expired in flight fails rather than being reported
as held. An initial claim whose response was lost replays its existing
reservation instead of incrementing `generation`, so a transport retry does
not cost you the lease you already hold.
Read `generation` back from this reply and carry it into `refreshQuotes`
rather than assuming it survived from an earlier call.
**Errors:** `400` (`launchId` or `ownerId` is not a UUID), `401`, `403`
(delegated-agent session), `404` (no such launch for this maker and
application), `409` (`Managed market is not ready for maker quotes`, or
`Managed market lease changed or expired; reconcile the lease`), `429` (the
claim costs one item against your order-creation budget), `500`, `503` (the
matching engine is unreachable).
Ask the engine to restate this launch's bounded quote set under a lease you
hold, and report what it now holds.
**Parameters:**
* `ownerId`: string — the same process UUID the lease was claimed with
* `generation`: `ManagedMarketFence` — the generation from `claimLease`
* `quoteRevision`: `ManagedMarketFence` — must be positive and at least the revision the engine already holds for this generation. A **lower** one is refused; re-sending the one it holds is an accepted idempotent no-op. Derive it from `claimLease`'s reply (`BigInt(lease.quoteRevision) + 1n`) rather than starting at `"1"`: a renewal keeps the generation's existing revision, so `"1"` merely restates the quote set while the held revision is exactly `1`, and is refused once it has advanced past that. Only a claim that bumped the generation resets the revision to `0`
**Returns:** `ManagedMarketQuoteStatus` — `launchId`, the acknowledged
`generation` and `quoteRevision`, `phase`, `bidNotional`, `askNotional`,
`oracleReady`, `readyToActivate` and `observedAt`.
`phase` on this reply is the **runtime** phase and takes only `"warming"`,
`"live"` or `"halted"` — a narrower set than the launch phase on an
assignment. Anything else the engine reports is answered as `503` with
`Managed market status unavailable; reconcile the lease`.
The refresh places one bounded bid and one bounded ask and costs **two** items
against your order-creation budget. The engine also enforces the launch's own
cadence and **refuses** a refresh that *advances* the revision less than
`refreshIntervalMs` after the last one — a rejection, not a silently dropped
no-op — so pace the loop on `refreshIntervalMs`. Re-sending the revision the
engine already holds is exempt: that equality check runs before the cadence
check, so an idempotent retry succeeds inside the window rather than being
refused.
**You cannot tell the engine's refusals apart, and there are two `409` paths.**
Before the engine is called at all, the handler re-checks the launch's
persisted readiness, so a launch already recorded as halted (or otherwise not
quotable) answers `409 Managed market is not ready for maker quotes`. Every
refusal that comes back *from* the engine — a stale revision, an early refresh,
an expired lease, a market the engine has halted since — is folded into a
single `409 Managed market lease changed or expired; reconcile the lease`. Both
are `409`, so never branch on the message: call `claimLease` and act on the
`generation` and `quoteRevision` it returns.
Prices, sizes, fees and the risk bucket are all server-derived — the request
carries no price, quantity or identity to override them.
**Treat an ambiguous failure as an unknown outcome.** On a timeout or a `503`
you do not know whether the intent reached the engine, so you do not know which
revision it now holds. Call `claimLease` and read the engine-observed
`quoteRevision` back before choosing the next one. Blindly re-sending the same
revision is not itself dangerous — the engine treats an equal revision as a
no-op and refuses a lower one — it simply tells you nothing, and leaves you
advancing from a number you never confirmed.
**Errors:** `400` (`launchId` / `ownerId` not a UUID, or
`quoteRevision must be positive`), `401`, `403` (delegated-agent session),
`404`, `409` (launch not ready to quote, or the lease changed or expired),
`429` (the two items this call costs against your order-creation budget — the
engine's cadence refusal is **not** a `429`; it arrives as the `409` above),
`500`, `503`.
## See Also
* [Managed Markets protobuf reference](/grpc/protos/managed-markets) — the gRPC service and message shapes
* [Rate limits](/developers/rate-limits) — the order-creation budget these calls draw on
# Margin Accounts
Source: https://docs.0xmonaco.com/sdk/typescript/margin-accounts
Fund and inspect parent margin accounts that back perp risk buckets
```typescript theme={null}
import { MonacoSDK } from "@0xmonaco/core";
// Initialize SDK with wallet client (see Initialization guide)
// Cursor paged: follow nextPageToken until it comes back empty.
await sdk.marginAccounts.listMarginAccounts({ pageSize: 20 });
await sdk.marginAccounts.getParentMarginAccountSummary();
await sdk.marginAccounts.getParentMarginAccountSummary({ tradingPairId });
// Funding
await sdk.marginAccounts.transferCollateralToParentMarginAccount({ asset, amount });
await sdk.marginAccounts.transferCollateralToRiskBucket({ asset, amount, tradingPairId, strategyKey? });
await sdk.marginAccounts.transferCollateralFromParentMarginAccount({ asset, amount });
await sdk.marginAccounts.getAvailableCollateral({ asset: "USDC" });
// Sub-accounts
await sdk.marginAccounts.createMarginSubAccount({ subaccountKey, label? });
await sdk.marginAccounts.transferMarginCollateral({ asset, amount, from, to });
await sdk.marginAccounts.transferCollateralToSubAccount(marginAccountId, amount);
await sdk.marginAccounts.transferCollateralFromSubAccount(marginAccountId, amount);
await sdk.marginAccounts.getMarginAccountPnl(marginAccountId);
await sdk.marginAccounts.closeMarginSubAccount(marginAccountId);
// Activity
await sdk.marginAccounts.getParentMarginAccountMovements(params?);
// Pre-trade check
await sdk.marginAccounts.simulateParentMarginOrderRisk(request);
await sdk.marginAccounts.simulateRiskBucketOrderRisk(request);
```
For high-level concepts, see [Perps Collateral](/developers/perps-collateral) for collateral, [Order Management](/developers/trade#risk-buckets-and-pre-trade-simulation) for risk simulation, and [Positions](/developers/positions#parent-accounts-and-risk-buckets) for account state.
***
## Lifecycle
Parent margin account creation is handled internally. Use `transferCollateralToParentMarginAccount` for first-time margin funding; it does not take a `tradingPairId` and does not open a risk bucket. Orders and risk simulation can use the accountless methods below without storing a `marginAccountId`.
Automatic parent creation and cross risk bucket provisioning return retryable
`503` / gRPC `UNAVAILABLE` if the sequencer refuses provisioning because its
lane is full. Back off before retrying; a transport timeout still requires
reconciling the enclosing operation's outcome. See [Error Handling](/sdk/typescript/error-handling).
Paginated list of the user's margin accounts. The result includes one row per margin account — the default (parent) margin account and every sub-account, closed sub-accounts included with `marginAccountState: "CLOSED"` — plus a separate row per risk bucket (risk bucket rows carry `riskBucketId`, `marginMode`, and `tradingPairId`). Every row carries its margin account's `subaccountKey` and `marginAccountState`.
Cursor-paginated; there is no page-number (offset) mode.
**Parameters:**
* `params?`: `ListMarginAccountsParams`
* `pageSize?`: number — 1 to 1000, defaults to 20; a larger value is rejected rather than clamped
* `pageToken?`: string — pagination cursor: omitted or `""` starts a walk from the lowest margin account id; a previous `nextPageToken` resumes it. There is no `page`: the SDK rejects one with a `ValidationError` before sending, and a raw REST request carrying it gets `400`. The walk seeks on `(marginAccountId, tradingPairId, riskBucketId, strategyKey)` — the parent row ahead of its own bucket rows — so a bucket that opens or closes mid-walk no longer shifts the rows behind it across the resume point. The strategy key is part of the key because an auto risk bucket the engine has not materialized yet carries no `riskBucketId`, and one account and pair can hold several strategies; such a row sorts under the id its bucket will carry once the engine projects it, so provisioning completing mid-walk cannot serve the same risk bucket twice. The filters must match the request that minted the token
* `state?`: string — keep only risk-bucket rows whose `riskBucketState` matches; the parent row has no state, so any filter excludes it
* `tradingPairId?`: string — filter to bucket rows scoped to a specific trading pair
**Returns:** `ListMarginAccountsResponse`:
* `accounts`: `MarginAccountSummary[]`
* `total`: number — exact; a user's accounts and buckets are a small bounded set
* `pageSize`: number
* `nextPageToken`: string — cursor for the next page, `""` once the walk is exhausted. Each page over-fetches one row, so an empty token is definitive
Live state of the authenticated wallet's parent margin account. Monaco resolves the parent account from the wallet and application scope; no `marginAccountId` lookup is required. Passing `tradingPairId` returns the summary for that pair's risk bucket under the parent — the risk bucket fields (`riskBucketId`, `marginMode`, `tradingPairId`) are populated on risk bucket summaries and left unset on the parent.
**Parameters:**
* `params?`: `GetMarginAccountSummaryParams`
* `tradingPairId?`: string — return the bucket summary scoped to this trading pair
**Returns:** `MarginAccountSummary`:
* `marginAccountId`: string
* `label?`: string
* `riskBucketId?`: string — set on risk bucket summaries; unset on the parent
* `marginMode?`: `"ISOLATED" | "CROSS"` — set on risk bucket summaries; unset on the parent
* `tradingPairId?`: string — trading pair this risk bucket is scoped to, if set
* `selectedTradingPairIds?`: string\[] — on the cross risk-bucket row only: the bucket's recorded scope. A pair joins when a cross order on it succeeds. After its positions close, removal is lazy: it remains listed until a later successful cross order on a different pair reconciles the bucket. Pairs declared before scope became derived follow the same delayed pruning after they trade and close. Omitted on the parent row and isolated rows; it may also be omitted when a cross bucket has no recorded scope
* `strategyKey?`: string — strategy tag, if set
* `subaccountKey?`: string — the margin account's sub-account key; `"default"` is the default (parent) margin account. On a risk bucket row, the key of the margin account that owns the bucket
* `marginAccountState?`: string — the margin account's state: `"ACTIVE"`, or `"CLOSED"` for a closed sub-account
* `riskBucketState?`: string — on risk bucket summaries only: `"NORMAL"` | `"PENDING_LIQUIDATION"` | `"LIQUIDATING"` | `"BAD_DEBT"` | `"CLOSED"`. Unset on the parent — a margin account has no liquidation lifecycle of its own; liquidation and bad debt live in its risk buckets
* `equity`: string
* `initialMarginRequired`: string
* `maintenanceMarginRequired`: string
* `freeCollateral`: string — trading-margin headroom for existing cross exposure
* `withdrawableCollateral`: string — the largest amount the withdrawal gate will accept without lowering a position's posted margin (on the parent margin account: deposits plus settled realized PnL and funding, less risk bucket allocations, and never more than `freeCollateral`; on a risk bucket: its allocation plus realized PnL, less funding paid, unrealized losses and the margin its positions and resting orders require, and zero unless the risk bucket is `NORMAL`; an unrealized gain never raises it). Size transfers — and new isolated/first-use positions, which allocate principal through the same gate — from this field, not `freeCollateral`. Rounded down to the collateral asset's decimals, so a transfer of exactly this amount is accepted.
* `totalPositionNotional`: string
* `unrealizedPnl`: string
* `realizedPnl`: string — on a risk bucket row, the risk bucket's cash balance (realized closes, funding, and transfers between a copy account's risk buckets land here), not trading profit; read trading PnL from position history and the portfolio PnL endpoints
* `updatedAt`: string — ISO 8601
* `maxRemovableCollateral?`: string — on a risk-bucket row, the largest removal the risk engine accepts when it may also lower the posted margin of the bucket's one open position: `withdrawableCollateral` plus that position's margin above its initial margin at the mark, capped by what the parent accepts. Everything past `withdrawableCollateral` comes out of the position's margin, raising its leverage and moving its liquidation price toward the mark, so use it to cap an isolated "remove margin" control and `withdrawableCollateral` for the part that leaves the position untouched. Equal to `withdrawableCollateral` on a cross bucket and on an isolated one with no open position or several. Present only on risk-bucket rows served from a live risk engine snapshot (absent on parent rows and fallback reads). Rounded down to the collateral asset's decimals.
* `availableOrderCollateral?`: string — the collateral still available to open **new** orders in this risk bucket: the largest increase in the bucket's worst-case order reserve the risk engine's admission gate accepts, automatic top-up from the parent's unallocated capacity included. Unlike `withdrawableCollateral` it credits an unrealized gain in full, and it already contains the bucket's live free collateral — don't add `freeCollateral` or `unrealizedPnl` on top (it can legitimately differ from this row's `freeCollateral`, a persisted reconstruction that can't see resting-order reserves). **Not additive across rows**: every bucket's figure includes the same parent headroom. Below the bucket's initial margin it is the parent headroom alone: a new order's own margin and fee are drawn in full and the bucket's existing gap stays (the parent never fills it). A requirement budget, not an order size — a reducing, reduce-only, or opposite-side order sheltered by a larger resting ladder needs less or none of it (and can be admitted when this is zero). Collateral, not notional (divide by the market's initial margin rate). Present only on risk-bucket rows served from a live risk engine snapshot — absent on parent rows and fallback reads, so handle `undefined`; the per-order authority remains `simulateRiskBucketOrderRisk`. Rounded down to the collateral asset's decimals, like `withdrawableCollateral`; it bounds order admission, not transfers.
* `copyFollowId?`: string — present only on a copy risk bucket row (a follow's copy account cross risk bucket and its isolated children): the id of the follow that funds it, one of your own follows. Omitted, never `null`, on every other row
* `copyLeaderHandle?`: string — on a copy risk bucket row, the followed leader's custom TraderCode handle, when they still hold one; omitted otherwise
**Example:**
```typescript theme={null}
const s = await sdk.marginAccounts.getParentMarginAccountSummary();
console.log(`Equity: ${s.equity}, Free: ${s.freeCollateral}`);
// Risk bucket summary for one trading pair under the parent:
const bucket = await sdk.marginAccounts.getParentMarginAccountSummary({
tradingPairId: btcPerpTradingPairId,
});
```
Legacy compatibility method for callers that already store a `marginAccountId`. Prefer `getParentMarginAccountSummary` for new integrations.
***
## Funding
Margin collateral must be a USDC-family asset in the engine's configured
registry. The gateway supplies matching raw and normalized amounts, and the
engine validates them against the registry's token decimals and symbol.
Transfers cannot reset a live margin account or risk bucket's lifecycle state.
**Deprecated.** This endpoint overloads two unrelated operations: with `tradingPairId` it allocates into that pair's isolated risk bucket (crediting the sole open position's stored margin), without it it performs a wallet-debiting deposit to the parent. Use `transferCollateralToRiskBucket` for bucket funding — which now applies the same position-margin credit — and `transferCollateralToParentMarginAccount` for wallet-to-margin deposits. Kept for backward compatibility; no removal date scheduled.
When `tradingPairId` is omitted the transfer is scoped to the parent account, even if an active cross-risk bucket exists.
**Parameters:**
* `marginAccountId`: string
* `request`: `TransferCollateralRequest`
* `asset`: string — e.g. `"USDC"`
* `amount`: string — decimal string in collateral units
* `tradingPairId?`: string — target a specific bucket under the parent; omit to target the parent
* `strategyKey?`: string — strategy tag for the bucket
**Returns:** `TransferCollateralResponse`:
* `movementId`: string
* `marginAccountId`: string
* `strategyKey?`: string — the bucket's strategy tag, if set
* `asset`: string
* `amount`: string
* `status`: string
* `newEquity`: string — equity after the transfer
* `newTotalCollateralValue`: string — total collateral value in the account after the transfer
* `newWithdrawableCollateral`: string — amount available to withdraw after the transfer, rounded down to the collateral asset's decimals
Use `newTotalCollateralValue` and `newWithdrawableCollateral` to update the UI immediately without a separate read.
Release collateral from a risk bucket, or move it from a known margin account back to the wallet.
Which of those happens depends on how the bucket is named:
* `tradingPairId` — releases that pair's **isolated** bucket. The collateral returns to the **parent margin account**, where it can be redeployed; it does not reach the wallet.
* `marginMode: "CROSS"` — releases the account's **cross** bucket, which has no trading pair and cannot be named any other way. Also returns to the parent. The two are mutually exclusive.
* neither — a parent-scoped transfer to the **wallet**. Prefer `transferCollateralFromParentMarginAccount` for new integrations.
Constrained by `withdrawableCollateral`, or for an isolated bucket with one open position by `maxRemovableCollateral` (the part past `withdrawableCollateral` comes out of the position's margin). Trying to withdraw more will fail with a 400.
**Parameters:**
* `marginAccountId`: string
* `request`: `TransferCollateralRequest`
* `asset`: string — e.g. `"USDC"`
* `amount`: string — decimal string in collateral units
* `tradingPairId?`: string — release that pair's isolated risk bucket to the parent; omit to target the parent
* `marginMode?`: `"ISOLATED" | "CROSS"` — pass `"CROSS"` to select the account's cross risk bucket, which has no trading pair and cannot be named by `tradingPairId`; the released collateral returns to the parent margin account. Mutually exclusive with `tradingPairId` — passing both is rejected client-side
* `strategyKey?`: string — strategy tag for the bucket
Same response shape as `transferCollateralToMarginAccount`, including `newTotalCollateralValue` and `newWithdrawableCollateral`.
Move collateral from the user's wallet into the parent margin account without specifying a `marginAccountId`. This endpoint does not accept `tradingPairId` and does not open a risk bucket.
**Parameters:**
* `request`: `TransferCollateralToParentMarginAccountRequest`
* `asset`: string — e.g. `"USDC"`
* `amount`: string — decimal string
**Returns:** Same `TransferCollateralResponse` shape as `transferCollateralToMarginAccount`.
**Example:**
```typescript theme={null}
await sdk.marginAccounts.transferCollateralToParentMarginAccount({
asset: "USDC",
amount: "500",
});
```
Move collateral into a specific risk bucket without specifying a `marginAccountId`. Monaco resolves the parent account and ensures the risk bucket exists automatically.
What the money becomes when the isolated bucket holds an open position is stated by `applyToPositionMargin` — the two intents want opposite fates for the same dollars, and either way the liquidation cushion improves identically:
* **Omitted or `true`** (the default is the human "Adjust Margin" gesture): the amount is **committed to the position's stored margin**. Effective leverage drops and stays down, initial margin required rises by exactly the amount, priced at the current mark (the stored position margin rises by `amount × entry price ÷ mark`), and free collateral is unchanged — new orders cannot spend it.
* **`false`**: a **plain allocation**. Free collateral rises and the next order can spend it. Programmatic funding loops that top a bucket up to a free-collateral target **must pass `false`** — under the default, each top-up converts into requirement and the loop never converges.
A flat isolated bucket has nothing to credit (both values are a plain allocation). The cross form does not take the field at all — cross risk is pooled at bucket equity, so `TransferCollateralToCrossRiskBucketRequest` types it `never` and the request validator rejects it even as `false`; omit it entirely on a cross transfer. (The raw REST and gRPC bodies are laxer, rejecting only an explicit `true`.) The response's `positionMarginCredited` reports what actually happened: `true` — the credit landed; `false` — it degraded to a plain allocation because the persisted position row was stale (close/reopen race; re-issue the Adjust Margin once the new row lands); absent — no credit was in play.
**Parameters:**
* `request`: `TransferCollateralToRiskBucketRequest` — a union on `marginMode`:
* **Isolated** (`marginMode` omitted or `"ISOLATED"`):
* `asset`: string — e.g. `"USDC"`
* `amount`: string — decimal string
* `tradingPairId`: string — UUID of the trading pair
* `strategyKey?`: string — omit to target the default risk bucket for the pair
* `marginAccountId?`: string — the margin account whose risk bucket is funded: omit (or pass the default margin account's id) for the default margin account, or name one of your open sub-accounts, which funds its own risk buckets from its own collateral
* `applyToPositionMargin?`: boolean — see above; omitted means `true` (commit to the open position's stored margin), `false` keeps the amount as spendable free collateral
* **Cross** (`marginMode: "CROSS"`):
* `asset`: string
* `amount`: string
* `marginAccountId?`: string — as for isolated: omit for the default margin account, or name one of your open sub-accounts to fund that sub-account's cross risk bucket
* nothing else. A cross transfer names no pairs: the cross bucket's scope is **derived from trading** — a pair joins the bucket when a cross order on it succeeds — so funding has nothing to declare. After that pair's positions close, removal is lazy: it remains listed until a later successful cross order on a different pair reconciles the bucket. Read the current scope from the cross bucket's row in `listMarginAccounts()` (`selectedTradingPairIds`) or from this call's response; the parent summary row carries no scope. Pairs declared before scope became derived follow the same delayed pruning after they trade and close. Because scope no longer needs declaring, `simulateParentMarginOrderRisk` and the pair-filtered summaries resolve a never-traded pair to the account's cross bucket instead of refusing it.
`selectedTradingPairIds` is no longer part of the cross request. It used to declare — and on every top-up **replace** — the bucket's scope, which meant a top-up that restated a partial list silently evicted every flat pair it forgot. The raw REST and gRPC bodies still accept the field for pre-deprecation clients and ignore it; the SDK types it `never` and its validator rejects it so the mistake is caught at compile time rather than believed to have taken effect.
**Returns:** Same `TransferCollateralResponse` shape as `transferCollateralToMarginAccount`.
**Example:**
```typescript theme={null}
await sdk.marginAccounts.transferCollateralToRiskBucket({
asset: "USDC",
amount: "500",
tradingPairId: btcPerpTradingPairId,
strategyKey: "btc-mm-strategy-a",
});
// Cross: fund the account's one cross bucket; scope follows your trading.
await sdk.marginAccounts.transferCollateralToRiskBucket({
asset: "USDC",
amount: "500",
marginMode: "CROSS",
});
```
Move collateral from the parent margin account back to the user's wallet without specifying a `marginAccountId`.
**Parameters:**
* `request`: `TransferCollateralFromParentMarginAccountRequest`
* `asset`: string — e.g. `"USDC"`
* `amount`: string — decimal string
**Returns:** Same `TransferCollateralResponse` shape as `transferCollateralFromMarginAccount`.
Check what's actually movable from the user's wallet into a margin account, and what can come back out. The two directions are separate fields: `marginTransferable` is wallet-to-margin capacity, `marginAvailableCollateral` is the largest amount that can currently leave the parent margin account.
**Parameters:**
* `params?`: `GetAvailableCollateralParams`
* `asset?`: string — defaults to USDC
**Returns:** `GetAvailableCollateralResponse`:
* `asset`: string
* `walletAvailable`: string — unlocked spot balance
* `walletLocked`: string — locked by spot orders / pending withdrawals
* `marginTransferable?`: string — eligible to transfer in
* `marginAvailableCollateral?`: string — the largest amount that can currently leave the parent margin account, matching `MarginAccountSummary.withdrawableCollateral`. Not trading headroom for existing cross exposure: it is bounded by deposits plus settled realized PnL and funding, less risk bucket allocations, as well as by margin requirements, and an unrealized gain does not raise it. The summary's `freeCollateral` remains the trading-margin headroom for existing cross exposure — but a **new** isolated or first-use position allocates principal through the same gate this field measures, so size those from this field (or `withdrawableCollateral`), not from `freeCollateral`.
***
## Sub-accounts
A wallet can hold several margin accounts per application, one per sub-account key. The key `"default"` is the default (parent) margin account — the only one that deposits, withdrawals and wallet transfers touch. Every sub-account holds its own collateral and its own risk buckets. The risk-bucket methods above (`transferCollateralToRiskBucket`, `simulateRiskBucketOrderRisk`) take an optional `marginAccountId` to act on a sub-account's own risk buckets; a copy-trading sub-account's risk buckets belong to its follow and are refused there. Sub-account management is denied to delegated-agent sessions. See [Perps Collateral](/developers/perps-collateral#margin-sub-accounts) for the end-to-end flow.
Open a margin sub-account keyed by `subaccountKey`. The default margin account must already exist (deposit or transfer collateral in first). Re-creating the key of an open sub-account returns it unchanged; re-creating the key of a closed sub-account re-opens the same account.
An owner may hold a limited number of open sub-accounts, set by their weighted 14-day volume tier — 10 at the first tier, rising with the fee-tier floors to 50 — or an admin-set override. A create past the cap is refused. Copy-trading sub-accounts do not count toward it.
**Parameters:**
* `request`: `CreateMarginSubAccountRequest`
* `subaccountKey`: string — up to 64 letters, digits, `_`, `-`, `.` or `:`. Keys starting with `copy:` are reserved, and `"default"` names the default margin account, so both are rejected
* `label?`: string — optional display label
**Returns:** `CreateMarginSubAccountResponse`:
* `marginAccountId`: string
* `subaccountKey`: string
* `marginAccountState`: string — `"ACTIVE"`
* `label?`: string
**Example:**
```typescript theme={null}
const hedge = await sdk.marginAccounts.createMarginSubAccount({ subaccountKey: "hedge", label: "Hedge book" });
```
Move collateral between the spot wallet and your margin accounts. Supported moves: wallet to the default margin account and back, and margin account to margin account (one atomic step, no wallet movement). A sub-account cannot exchange collateral with the wallet directly — route it through the default margin account. Risk-bucket endpoints are refused on this method; fund a risk bucket with `transferCollateralToRiskBucket`.
**Parameters:**
* `request`: `TransferMarginCollateralRequest`
* `asset`: string — e.g. `"USDC"`; required for a wallet move, accepted and ignored on a margin-account-to-margin-account move, which stays in USD
* `amount`: string — positive decimal string
* `from`: `CollateralTransferEndpoint` — `{ wallet: true }` or `{ marginAccountId, riskBucketId? }`
* `to`: `CollateralTransferEndpoint` — same shape; `from` and `to` must be different margin accounts, and at least one side must be a margin account
**Returns:** `TransferMarginCollateralResponse`:
* `movementId`: string
* `amount`: string
* `fromMarginAccountId?`: string — the source margin account, when the source is one
* `toMarginAccountId?`: string — the destination margin account, when the destination is one
* `toRiskBucketId?`: string — the destination risk bucket, when the destination is one
**Example:**
```typescript theme={null}
const parent = await sdk.marginAccounts.getParentMarginAccountSummary();
await sdk.marginAccounts.transferMarginCollateral({
asset: "USDC",
amount: "250",
from: { marginAccountId: parent.marginAccountId },
to: { marginAccountId: hedge.marginAccountId },
});
```
Move collateral from your default margin account to one of your sub-accounts, in one atomic step. The same move as `transferMarginCollateral` from the default margin account to the sub-account. The default margin account itself is refused; fund it with `transferCollateralToParentMarginAccount`.
**Parameters:**
* `marginAccountId`: string — UUID of one of your open sub-accounts
* `amount`: string — positive decimal string
**Returns:** `TransferMarginCollateralResponse` with `fromMarginAccountId` the default margin account and `toMarginAccountId` the sub-account.
**Example:**
```typescript theme={null}
await sdk.marginAccounts.transferCollateralToSubAccount(hedge.marginAccountId, "250");
```
Move collateral from one of your sub-accounts back to your default margin account, in one atomic step. The amount must fit the sub-account's free collateral.
**Parameters:**
* `marginAccountId`: string — UUID of one of your open sub-accounts
* `amount`: string — positive decimal string
**Returns:** `TransferMarginCollateralResponse` with `fromMarginAccountId` the sub-account and `toMarginAccountId` the default margin account.
One of your margin accounts' own PnL — the default margin account or a sub-account. Transfers between your accounts are flows, not PnL, so the figures of all your margin accounts sum to the owner-wide figure.
**Parameters:**
* `marginAccountId`: string — UUID of one of your margin accounts
**Returns:** `MarginAccountPnl`:
* `marginAccountId`: string
* `realizedPnl`: string — settled on the account plus still held in its open risk buckets
* `unrealizedPnl`: string — the account's open positions repriced at the latest risk marks
Close a flat sub-account in one call. The sub-account must hold no position and no resting order, conditional order or TWAP. Its flat, solvent risk buckets close in the same step, and all of its remaining collateral moves to the default margin account. A risk bucket in liquidation or bad debt refuses the close, and nothing moves. The default margin account cannot be closed, and a copy-trading sub-account is closed by stopping its follow.
**Parameters:**
* `marginAccountId`: string — UUID of one of your open sub-accounts
**Returns:** `CloseMarginSubAccountResponse`:
* `marginAccountId`: string — the closed sub-account
* `defaultMarginAccountId`: string — the default margin account that received the remaining collateral
* `amount`: string — collateral moved to the default margin account
***
## Activity
Collateral movements (transfers in / out) recorded against the authenticated wallet's parent margin account. PnL, funding, and fees are tracked through other endpoints (`getPaginatedUserMovements` and `GET /api/v1/accounts/funding-payments`).
**Parameters:**
* `params?`: `GetMarginAccountMovementsParams`
* `movementType?`: `"TRANSFER_IN"` | `"TRANSFER_OUT"`
* `pageSize?`: number — 1 to 1000, defaults to 20
* `pageToken?`: string — cursor pagination. Omitted or `""` starts the walk at the newest row; otherwise pass the previous response's `nextPageToken`. There is no `page`: the SDK rejects one with a `ValidationError` before sending, and a raw REST request carrying it gets `400`. The filters must match the request that minted the token.
**Returns:** `GetMarginAccountMovementsResponse`:
* `movements`: `MarginAccountMovement[]` — each with `movementId`, `movementType`, `asset`, `amount`, `createdAt`; newest-first
* `pageSize`: number
* `nextPageToken`: string — cursor for the next page, `""` once the walk is exhausted. Each page over-fetches one row, so an empty token is definitive: follow a non-empty token, stop on an empty one
No item count: the underlying ledger is too large to count behind this filter.
Legacy compatibility method for callers that already store a `marginAccountId`. Prefer `getParentMarginAccountMovements` for new integrations.
***
## Pre-trade simulation
Preflight a perp order against the authenticated wallet's parent margin account without placing it. Monaco resolves the parent account from the wallet and application scope.
**Parameters:**
* `request`: `SimulateOrderRiskRequest`
* `tradingPairId`: string
* `side`: `"BUY" | "SELL"`
* `positionSide?`: `"LONG" | "SHORT" | "NONE"` — deprecated compatibility field; if supplied, it must agree with `side`
* `orderType`: `"LIMIT" | "MARKET"`
* `price?`: string — required for LIMIT
* `quantity`: string
* `leverage`: string — decimal string, e.g. `"10"`; required unless `reduceOnly` is `true`
* `reduceOnly?`: boolean
* `slippageToleranceBps?`: number — MARKET only, `0`–`1000`, the rule the server applies to a placed MARKET order (a LIMIT preview carrying it is rejected with the placement's message). Tightens the previewed walk to the band the real order will run under, so `expectedMatchResult` and `estimatedFee` price under it. Omit for the placement default; it never widens the band. To carry a previewed value into placement, note that `placeMarketOrder` and the batch helpers take `slippageTolerance` as a **ratio** (`0.05` = 500 bps): pass `slippageToleranceBps / 10_000` — a `slippageToleranceBps` key on those calls is not forwarded. On a first-use isolated preview the value is also forwarded to the bucket's auto-funding for parity with placement; that path floors it at 1,000 bps, so today it does not change the funded figure.
**Returns:** `SimulateOrderRiskResponse`:
* `accepted`: boolean
* `rejectReason?`: string — human-readable when `accepted: false`
* `marginAccountId`: string — the resolved margin account the simulation ran against
* `strategyKey?`: string — the bucket's strategy tag, if set
* `marginMode?`: `"ISOLATED" | "CROSS"` — the resolved risk-bucket mode, if set
* `riskBucketId?`: string — the risk bucket the simulation resolved against, if any
* `selectedTradingPairIds?`: string\[] — cross previews only, never a caller's list: from `simulateRiskBucketOrderRisk` the derived scope the preview ran against (the cross bucket's recorded pairs — including any still listed after their positions closed — every pair with an open position in it, and the previewed pair; the previewed pair alone before the first cross order); from `simulateOrderRisk` and `simulateParentMarginOrderRisk`, the recorded pairs plus the previewed pair
* `equityAfter`: string
* `initialMarginRequiredAfter`: string
* `maintenanceMarginRequiredAfter`: string
* `freeCollateralAfter`: string
* `estimatedFee?`: string — what the order pays in fees once its whole size has executed: the taker rate (plus the application's additional taker fee) on the quantity that would cross immediately, the maker rate on the quantity that would rest. One figure whether the order fills, rests, or splits between the two. **Signed** — negative is a net rebate, which is what a resting order earns on a pair whose maker fee is negative. Populated even when `accepted: false`, and for reduce-only orders. Assumes the resting part eventually fills. Absent — never `0` — when the engine could not price the order; treat absence as unknown, not free.
* `estimatedLiquidationPrice?`: string — isolated simulated position/risk-bucket threshold; cross conditional per-target-position estimate, not a whole-account scalar. Absent or blank means unavailable, never `0`.
* `expectedMatchResult?`: MatchResult — the simulated match the engine ran to admit the order: the same `MatchResult` shape a placement's `matchResult` carries, from the same walk over the live book at the moment of the preview, under `slippageToleranceBps` merged tighter-wins with the market's protective band (1,000 bps by default; see [Perp price band](/trading-mechanics/perps#perp-price-band)) and your wallet's self-trade-prevention default (the preview runs under your wallet, as placement does). Not a client-side re-walk, so preflight and placement read the same numbers. `totalFilled` is what would execute; `remainingQuantity` what the band or the depth leaves unfilled — the partial-fill indicator, a MARKET preview with a non-zero remainder is a warning that the size does not clear the previewed book — placement runs against a later book, so read the placement's own status and quantities rather than treating this as the fill it will get; `averageFillPrice` and `executionPriceRange.worstPrice` are the fill levels (highest ask a BUY takes, lowest bid a SELL hits), `null` when nothing crosses; `status` is the status placement would end in — `FILLED` for a full fill, `CANCELLED` for a MARKET order the band or the depth cuts short (a MARKET remainder cannot rest, so the fills execute and the rest is cancelled; read `remainingQuantity` for the partial fill), `SUBMITTED` for a LIMIT that rests, `PARTIALLY_FILLED` for a LIMIT that crosses partly and rests the remainder; `actualSlippageBps` is the realized slippage against `referencePrice` — for a LIMIT or IOC preview, against the limit price, `null` when every fill improved on it; `maxSlippageBps` echoes your tolerance. A preview cannot carry `postOnly`, TP/SL legs or a per-order self-trade-prevention override, so a post-only order that would cross is refused by placement, not here. Present only when `accepted` is true; `null` on every refused preview (`accepted: false` with a `rejectReason`), including the post-match maker-risk rejection — treat absence as unknown, not as a zero fill.
* `referencePrice?`: string — the touch on the taking side when a MARKET preview ran (best ask for a BUY, best bid for a SELL), which `expectedMatchResult` and its `actualSlippageBps` are measured from. Present only when `accepted` is true and the preview is a MARKET order; a LIMIT or IOC preview is measured against its own limit price and publishes nothing here.
These price against the book **right now**. A take-profit or stop-loss closes later, on a different book, under the band a triggered market leg carries (1,200 bps by default; see [Perp price band](/trading-mechanics/perps#perp-price-band)) and anchored at trigger time — so to size a TP/SL target, preview the close as a reduce-only MARKET order of the position's size and apply `expectedMatchResult.actualSlippageBps` to your **trigger** price as an impact figure, rather than displaying this call's absolute `expectedMatchResult.averageFillPrice`. The fee tier the preview reads can lag the engine's own copy by one refresh interval, so `estimatedFee` and the fee settled moments later can differ by a tier step in that window.
**Example:**
```typescript theme={null}
const risk = await sdk.marginAccounts.simulateParentMarginOrderRisk({
tradingPairId,
side: "BUY",
orderType: "LIMIT",
price: "2000",
quantity: "1.5",
leverage: "10",
});
if (!risk.accepted) {
showError(risk.rejectReason);
return;
}
const estimatedLiquidationPrice = risk.estimatedLiquidationPrice?.trim();
renderLiquidationPrice(
estimatedLiquidationPrice
? risk.marginMode === "CROSS"
? `Conditional liquidation price: ${estimatedLiquidationPrice}`
: estimatedLiquidationPrice
: "unavailable",
);
```
Common pattern: run on every order-form change with debouncing, and disable the place-order button when `accepted === false`.
Legacy compatibility method for callers that already store a `marginAccountId`. Prefer `simulateParentMarginOrderRisk` for new integrations.
Preflight an order against a risk bucket without storing a `marginAccountId`. Accepts both isolated and cross modes via a discriminated union on `marginMode`.
**Isolated** (`marginMode?: "ISOLATED"`):
* `tradingPairId`: string
* `marginMode?`: `"ISOLATED"` — default when omitted
* `strategyKey?`: string — omit for the default risk bucket
* `marginAccountId?`: string — the margin account previewed: omit for the default margin account, or name one of your open sub-accounts to preview against that sub-account's own risk buckets
* `side`, `orderType`, `price?`, `quantity`, `leverage?`, `reduceOnly?` — `leverage` may be omitted only when `reduceOnly` is `true`; otherwise the SDK raises `ValidationError` (a raw REST or gRPC caller gets `400` / `INVALID_ARGUMENT`)
**Cross** (`marginMode: "CROSS"`):
* `tradingPairId`: string — the pair being previewed
* `marginMode`: `"CROSS"` — required
* `marginAccountId?`: string — as for isolated: omit for the default margin account, or name one of your open sub-accounts
* `side`, `orderType`, `price?`, `quantity`, `leverage?`, `reduceOnly?` — as for isolated, `leverage` is required unless `reduceOnly` is `true`
* nothing else. A cross preview names no pairs: the cross bucket's scope is **derived from trading** — a pair joins the bucket when a cross order on it succeeds; after its positions close, removal is lazy: it remains listed until a later successful cross order on a different pair reconciles the bucket — so the previewed pair is in scope by construction and there is nothing to declare. The response's `selectedTradingPairIds` reports the derived scope the preview ran against: the active cross bucket's recorded pairs (including any still listed after their positions closed), every pair with an open position in it, and the previewed pair (the previewed pair alone before the first cross order).
`selectedTradingPairIds` is no longer part of the cross request. It used to be required (non-empty, containing `tradingPairId`, covering every pair with an open cross position) while the server never priced anything from it — the engine prices the positions it holds whatever the caller declared — so it only ever produced 400s. The raw REST and gRPC bodies still accept the field for pre-deprecation clients and ignore it; the SDK types it `never` and its validator rejects it so the mistake is caught at compile time rather than believed to have taken effect.
Isolated simulation works before that pair's risk bucket exists: the API models the first-use bucket virtually, created empty exactly as live placement creates it, and the simulated admission draws what the order needs from the parent margin account. Cross simulation likewise works before the user has placed their first cross order — the API resolves the deterministic cross risk bucket and simulates the same draw, matching live placement.
For cross simulation, `estimatedLiquidationPrice` is conditional per target position, not a whole-account scalar: it varies only the target position's mark while all other marks in the cross risk bucket remain unchanged. Other position marks, funding, realized PnL, fees/reserves, and collateral can change it. Render it as a conditional liquidation price; an absent or blank value is unavailable, never `0`.
**Returns:** Same `SimulateOrderRiskResponse` shape as `simulateOrderRisk`.
**Isolated example:**
```typescript theme={null}
const risk = await sdk.marginAccounts.simulateRiskBucketOrderRisk({
tradingPairId: btcPerpTradingPairId,
strategyKey: "btc-mm-strategy-a",
side: "BUY",
orderType: "LIMIT",
price: "65000",
quantity: "0.5",
leverage: "10",
});
```
**Cross example:**
```typescript theme={null}
const risk = await sdk.marginAccounts.simulateRiskBucketOrderRisk({
tradingPairId: btcPerpTradingPairId,
marginMode: "CROSS",
side: "BUY",
orderType: "LIMIT",
price: "65000",
quantity: "0.5",
leverage: "10",
});
if (risk.accepted) {
const estimatedLiquidationPrice = risk.estimatedLiquidationPrice?.trim();
renderLiquidationPrice(
estimatedLiquidationPrice
? `Conditional liquidation price: ${estimatedLiquidationPrice}`
: "unavailable",
);
}
```
***
## Errors
* `400` on `transferCollateralFromParentMarginAccount` or `transferCollateralFromMarginAccount` when `amount` exceeds `withdrawableCollateral`
* `400` on `simulateParentMarginOrderRisk`, `simulateRiskBucketOrderRisk`, `simulateOrderRisk`, and order placement: `accepted: false` with `rejectReason` (e.g., "Requested leverage exceeds max leverage for trading pair"). Market-order simulations apply the same 1,000 bps price band as placement and can return a `rejectReason` for no liquidity inside the band or no usable reference price
* `400` on `simulateParentMarginOrderRisk`, `simulateRiskBucketOrderRisk` and `simulateOrderRisk` when `slippageToleranceBps` is sent on a non-MARKET preview or falls outside `0`–`1000` (it is an integer bps value; a negative tolerance is refused by the same schema as one above `1000`) — the SDK raises `ValidationError` before the request is sent; a raw REST or gRPC caller gets placement's own message (gRPC `INVALID_ARGUMENT`)
* `400` on `createMarginSubAccount` when the key is malformed, reserved (`copy:`) or `"default"` (the SDK raises `ValidationError` before the request is sent), when the default margin account does not exist yet, or when the owner is at their open sub-account cap
* `400` on `closeMarginSubAccount` when the account is the default margin account, a copy-trading sub-account, or already closed; when it still holds a position, resting order, conditional order or TWAP; or when one of its risk buckets is in liquidation, in bad debt, or insolvent
* `400` on `transferMarginCollateral` when an endpoint does not name exactly one of `wallet` and `marginAccountId`, both sides are the wallet, either side names a risk bucket, a wallet transfer names a sub-account rather than the default margin account, `from` and `to` are the same account, or either account is closed
* `400` on `transferCollateralToRiskBucket` and `simulateRiskBucketOrderRisk` when `marginAccountId` names a closed sub-account or a copy-trading sub-account
* `400` on `transferCollateralToSubAccount` and `transferCollateralFromSubAccount` when `marginAccountId` is the default margin account, the sub-account is closed, or the amount exceeds the source's free collateral
* `403` on `createMarginSubAccount`, `closeMarginSubAccount`, `transferMarginCollateral`, `transferCollateralToSubAccount` and `transferCollateralFromSubAccount` for delegated-agent sessions
* `403` when the account belongs to a different user
* `404` when the `marginAccountId` does not exist. The sub-account methods (`closeMarginSubAccount`, `getMarginAccountPnl`, `transferMarginCollateral`, `transferCollateralToSubAccount`, `transferCollateralFromSubAccount`) and the `marginAccountId` of `transferCollateralToRiskBucket` / `simulateRiskBucketOrderRisk` return the same `404` for another user's account as for an unknown one
* `429` on `createMarginSubAccount`, `closeMarginSubAccount`, `transferMarginCollateral`, `transferCollateralToSubAccount` and `transferCollateralFromSubAccount` — they draw from the per-user movement budget that withdrawals and collateral transfers share; `getMarginAccountPnl` draws from the read budget. REST carries the retry horizon in `details.retryAfter`; gRPC returns `RESOURCE_EXHAUSTED` with a `RetryInfo` detail
* `503` on `createMarginSubAccount` when no matching engine connection is configured or the new account has not been projected yet; retry with the same key, which is idempotent for an open sub-account. `closeMarginSubAccount` also returns `503` when no matching engine connection is configured
* `500` on `createMarginSubAccount` and `closeMarginSubAccount` when the call to the matching engine fails in transit; a create can be retried with the same key
* `429` on `simulateParentMarginOrderRisk`, `simulateRiskBucketOrderRisk` and `simulateOrderRisk` — every authenticated read draws one request from the account's read budget, and each preview also draws one from the account family's dedicated preview budget. Enforcement of each is gated per environment by its own flag (`read_rate_limit_enforced`, `order_risk_preview_rate_limit_enforced`), both **off by default**: until a flag is enabled its budget is measured and over-budget requests are admitted, so treat the `429` as a contract to handle rather than one you can rely on observing. When enforced, REST carries the retry horizon in `details.retryAfter` while gRPC returns `RESOURCE_EXHAUSTED` with a `google.rpc.RetryInfo` detail — the hint is transport-specific, so a gRPC caller reads `RetryInfo` rather than looking for a `retryAfter` field. See [Rate limits](/developers/rate-limits#reads)
For end-to-end usage and UI patterns, see [Perps Collateral](/developers/perps-collateral), [Order Management](/developers/trade#risk-buckets-and-pre-trade-simulation), and [Positions](/developers/positions).
# Market Data
Source: https://docs.0xmonaco.com/sdk/typescript/market
Query trading pairs and market data
```typescript theme={null}
import { MonacoSDK } from "@0xmonaco/core";
// Initialize SDK with wallet client (see Initialization guide)
// Trading pairs
await sdk.market.getPaginatedTradingPairs(params?); // optional: { category: "crypto" | "equities" | "commodities" | "fx" }
await sdk.market.getTradingPair(tradingPairId);
await sdk.market.getTradingPairBySymbol(symbol);
// Market screener
await sdk.market.getScreener(params?); // optional: { category: "crypto" | "equities" | "commodities" | "fx" }
// Exchange-wide life-to-date statistics (no auth)
await sdk.market.getMarketStats();
// Market metadata
await sdk.market.getMarketMetadata(pairId);
// Candlestick data (requires trading pair UUID)
const pair = await sdk.market.getTradingPairBySymbol('BTC/USDC');
await sdk.market.getCandlesticks(pair.id, interval, { startTime, endTime, limit, priceType }); // priceType: "trade" (default) | "mark"
// Orderbook snapshot (REST API)
await sdk.orderbook.getOrderbook(tradingPairId, options?);
// Historical trades (REST API)
await sdk.trades.getTrades(tradingPairId, options?);
await sdk.trades.getTradeById(tradeId);
```
## Trading Pairs
An **operator-managed market** is public only while its launch's **durable runtime projection** reads `live` — the coordinator's own launch phase is not what publishes it, so a launch whose phase is already `live` stays hidden whenever its runtime projection is anything else, halted included. Until then it is excluded from pair discovery, pair detail, the screener and the public price reads, and asking for inactive markets does not surface it either. A pair with no managed launch is an ordinary instrument and keeps its existing visibility, so this changes nothing about the markets that trade today. The maker side of such a launch is driven through [`sdk.managedMarkets`](/sdk/typescript/managed-markets).
Fetch paginated list of trading pairs.
**Parameters:** all optional
* `page?`: number
* `pageSize?`: number
* `category?`: `"crypto" | "equities" | "commodities" | "fx"` — filter by asset class
Each `TradingPair` includes a `category` field indicating its asset class,
and `baseAssetName` / `quoteAssetName` — the human-readable asset display
names from the asset registry (e.g. `"Robinhood Markets"` for `HOOD`,
`"USDC"` for `USDC`). Use them for name-based market search (a query like
"Robinhood" can match the `HOOD` market) and to render display names next to
tickers. Also present on `getTradingPair` and `getTradingPairBySymbol`.
`isolatedOnly` (boolean) marks an isolated-only market: cross margin may only
reduce a position there. A `CROSS` order that is not reduce-only is rejected
with the error code `CROSS_MARGIN_NOT_ALLOWED`; open the position with
`marginMode: "ISOLATED"`. Also present on `getTradingPair` and
`getTradingPairBySymbol`.
Fetch a single trading pair by its UUID. Returns the unwrapped `TradingPair`. Rejects with an `APIError` (`404`) when no pair with that UUID exists — it never resolves `undefined`.
**Parameters:**
* `tradingPairId`: string - Trading pair UUID
**Example:**
```typescript theme={null}
const pair = await sdk.market.getTradingPair('a1b2c3d4-...');
console.log(`${pair.symbol} tick size: ${pair.tickSize}`);
console.log(`${pair.symbol} quantity step: ${pair.quantityStepSize}`);
```
Get a specific trading pair by symbol. Resolves `undefined` on a miss — it never throws for an unknown symbol, so guard the result before dereferencing. Note the failure modes are opposite: `getTradingPair` rejects, `getTradingPairBySymbol` resolves `undefined`. The lookup scans only the first 100 pairs (optionally filtered by `marketType`); use `getPaginatedTradingPairs` directly if the listing ever exceeds that.
**Parameters:**
* `symbol`: string - Trading pair symbol (e.g. `"BTC/USDC"`)
* `marketType?`: `TradingMode` - Restrict the lookup to `"SPOT"` or `"MARGIN"` pairs
## Market Screener
Fetch the paginated market screener. Each row carries 1h/24h/7d quote-volume and price-change windows plus a 7-day daily snapshot. Results are sorted by 24h quote volume descending, with nulls last.
**Parameters:** `params?` - all optional
* `page?`: number - Page number (min 1, default 1)
* `pageSize?`: number - Items per page (min 1, max 100, default 50)
* `marketType?`: `"SPOT" | "MARGIN"` - Filter by market type
* `isActive?`: boolean - Filter by active status; defaults to `true` so delisted pairs are hidden. The managed-market fence is an **additional** gate rather than an override of this filter: a managed launch is hidden until its runtime projection is live, and once live its pair is active, so `isActive: false` still excludes it
* `category?`: `"crypto" | "equities" | "commodities" | "fx"` - Filter by asset class
**Returns:** `GetScreenerResponse`:
* `items`: `ScreenerItem[]`
* `page`: number
* `pageSize`: number
* `total`: number
* `totalPages`: number
Each `ScreenerItem`:
* `tradingPairId`: string - Trading pair UUID
* `symbol`: string - Trading pair symbol (e.g. "BTC/USDC")
* `category`: string - Asset class: `"crypto"`, `"equities"`, `"commodities"`, or `"fx"`
* `baseIconUrl`: string - Base token icon URL
* `quoteIconUrl`: string - Quote token icon URL
* `lastPrice`: string | null - Most recent close price (null if no trades yet)
* `lastPriceTimestamp`: string | null - Timestamp of the last candle (ms since epoch, as string; null if no trades yet)
* `quoteVolume1h`: string | null - Quote-token volume over the last 1 hour (null if insufficient history)
* `quoteVolume24h`: string | null - Quote-token volume over the last 24 hours (null if insufficient history)
* `quoteVolume7d`: string | null - Quote-token volume over the last 7 days (null if insufficient history)
* `priceChangePercent1h`: string | null - Percent price change over the last 1 hour (null if insufficient history)
* `priceChangePercent24h`: string | null - Percent price change over the last 24 hours (null if insufficient history)
* `priceChangePercent7d`: string | null - Percent price change over the last 7 days (null if insufficient history). Screener rows carry percent changes only — for an absolute 24h change, read `priceChange24h` from `getMarketMetadata` / `getPerpMarketSummary`, or estimate it from `lastPrice` and `priceChangePercent24h` (approximate, since the percentage is rounded)
* `snapshot7d`: `ScreenerSnapshotPoint[]` - Up to 7 UTC-day buckets (oldest first); empty when less than 1 day of history. Each point is `{ bucketStart, quoteVolume, priceChangePercent, openInterestNotionalAvg }`. The daily average is `string | null` and needs at least 95% of the completed UTC day's 1440 minutes (1368) to be valued.
* `totalQuoteVolumeLtd`: string - Cumulative quote-token volume since inception (decimal string; `"0"` if no trades)
* `totalTradeCountLtd`: number - Cumulative trade count since inception (`0` if no trades)
* `latestFundingRate`: string | null - Latest capped settled funding rate (decimal fraction per window); `null` for spot pairs and perps with no settled window yet
* `lastFundingTime`: string | null - Close time of the latest settled funding window (ms since epoch, as string); `null` for spot pairs and perps with no settled window yet
* `openInterestBase`: string | null - Base-asset open interest; `null` for spot pairs, `"0"` for a margin pair with no open positions
* `openInterestNotional`: string | null - Open interest notional in quote units, valued at the pair's latest trade price (a snapshot — for a mark-accurate notional use `getPerpMarketSummary` or `getOpenInterest`); `null` for spot pairs, `"0"` for a margin pair with no open positions
* `openInterestNotionalAvg1h`, `openInterestNotionalAvg24h`, `openInterestNotionalAvg7d`: string | null - Arithmetic averages of the valued completed UTC minute observations in the last 60, 1440, or 10080 minutes ending at the current minute boundary, published once at least 95% of those minutes (57, 1368, or 9576) are valued. Each observation is gross open position size multiplied by latest trade price in quote units, the same price `openInterestNotional` uses; a minute without a trade price is unvalued rather than zero. `null` for spot or below that coverage; known zero is `"0"`. No historical backfill; windows warm after deployment.
**Example:** Install `decimal.js` (`npm install decimal.js`) to compute surge directly from decimal strings.
```typescript theme={null}
import Decimal from "decimal.js";
const screener = await sdk.market.getScreener({ marketType: "MARGIN", pageSize: 50 });
screener.items.forEach((row) => {
const average7d = row.openInterestNotionalAvg7d;
const surge = row.openInterestNotionalAvg24h !== null && average7d !== null && new Decimal(average7d).gt(0)
? new Decimal(row.openInterestNotionalAvg24h).div(average7d).toString()
: null;
console.log(`${row.symbol}: 24h/7d-average OI ${surge ?? "n/a"}`);
});
```
## Market Metadata
Get comprehensive market metadata including current price and 24-hour statistics
**Parameters:**
* `tradingPairId`: string - Trading pair UUID
**Returns:** `MarketMetadata`:
* `symbol`: string - Trading pair symbol (e.g., "BTC/USDC")
* `lastPrice`: string - Current price (8 decimal precision)
* `lastPriceTimestamp`: number - Unix timestamp (ms)
* `high24h`: string | null - 24h high price
* `low24h`: string | null - 24h low price
* `volume24h`: string | null - 24h trading volume in base-asset units
* `quoteVolume24h?`: string | null - 24h trading volume in quote-token (USDC) units
* `priceChange24h`: string | null - Absolute price change
* `priceChangePercent24h`: string | null - Percentage change
* `marketInitializationTimestamp`: number | null - Market start time
* `totalBaseVolumeLtd`: string - Cumulative base-token volume since inception (decimal string; `"0"` if no trades)
* `totalQuoteVolumeLtd`: string - Cumulative quote-token volume since inception (decimal string; `"0"` if no trades)
* `totalTradeCountLtd`: number - Cumulative trade count since inception (`0` if no trades)
**Example:**
```typescript theme={null}
const pair = await sdk.market.getTradingPairBySymbol('BTC/USDC');
const metadata = await sdk.market.getMarketMetadata(pair.id);
console.log(`${metadata.symbol}: $${metadata.lastPrice}`);
console.log(`24h Change: ${metadata.priceChangePercent24h}%`);
console.log(`24h High: $${metadata.high24h}`);
console.log(`24h Low: $${metadata.low24h}`);
```
## Exchange Statistics
Fetch exchange-wide life-to-date cumulative statistics. No authentication required.
**Returns:** `MarketStats`:
* `totalQuoteVolumeLtd`: string - Cumulative quote-token (notional) volume across all trading pairs, as a decimal string in display units
* `totalTradeCountLtd`: number - Cumulative trade count across all trading pairs
Volume is summed across all pairs; base volume is intentionally omitted because base quantities are not comparable across different base assets. Pairs with no trades contribute `"0"` / `0`.
**Example:**
```typescript theme={null}
const stats = await sdk.market.getMarketStats();
console.log(`All-time volume: ${stats.totalQuoteVolumeLtd} USDC`);
console.log(`All-time trades: ${stats.totalTradeCountLtd}`);
```
## Candlestick Data
Fetch OHLCV candlestick data for a trading pair.
**Parameters:**
* `tradingPairId`: string - Trading pair UUID (use `getTradingPairBySymbol` to get this)
* `interval`: `"1m" | "5m" | "15m" | "1h" | "4h" | "1d"`
* `params?`: Optional query parameters
* `startTime?`: number - Unix timestamp in ms (for forward pagination)
* `endTime?`: number - Unix timestamp in ms (for backward pagination)
* `limit?`: number - Max candlesticks to return (default: 350, max: 500)
* `priceType?`: `"trade" | "mark"` - price series to chart. `"trade"` (default) is the trade-derived OHLCV series; `"mark"` is the close-only mark-price series for margin pairs, where each candle's open/high/low/close are the minute's closing mark and volume is `0`. `"mark"` on a spot pair returns a `400`
**Example:**
```typescript theme={null}
const pair = await sdk.market.getTradingPairBySymbol('BTC/USDC');
// Get latest 100 candles
const candles = await sdk.market.getCandlesticks(pair.id, '1h', { limit: 100 });
// Get candles for a time range
const historicalCandles = await sdk.market.getCandlesticks(pair.id, '5m', {
startTime: Date.now() - 24 * 60 * 60 * 1000,
endTime: Date.now()
});
```
## Perp Market Data
Perp-specific market endpoints. For margin concepts, see [Margin](/trading-mechanics/perps).
Static configuration for a perp pair: leverage bounds, base margin ratios, funding interval, risk tiers, liquidation fee.
**Returns — `PerpMarketConfig`:**
* `tradingPairId`, `symbol`: string
* `minLeverage`, `maxLeverage`: string (decimal)
* `initialMarginRatio`, `maintenanceMarginRatio`: string
* `fundingIntervalSeconds`: number
* `liquidationFeeBps?`: string
* `riskTiers`: `RiskTier[]`, ascending by `lowerBoundNotional` — each `{ tierLevel, lowerBoundNotional, maxLeverage?, initialMarginRatio, maintenanceMarginRatio, initialMarginDeduction, maintenanceMarginDeduction, maxPositionNotional? }`. `maxPositionNotional` is the most USD notional a position may hold at that tier's `maxLeverage`: the next tier's `lowerBoundNotional`, or the market's position cap on the top tier, and absent there when the market is uncapped. Below the top tier the cap is exclusive, since a notional at the next bound already sits in the next tier. `maxPositionSize` is deprecated and no longer set. `initialMarginDeduction`/`maintenanceMarginDeduction` keep the ladder floor and maintenance margin continuous at a tier bound: `IM_floor(N) = N x initialMarginRatio - initialMarginDeduction`, `MM(N) = N x maintenanceMarginRatio - maintenanceMarginDeduction` for a position of notional `N` in that tier. Actual position initial margin is the greater of `IM_floor(N)` and the selected-leverage initial margin scaled to the current mark. A position's tier is the last one whose `lowerBoundNotional` is at or below its notional. See [Margin ladders](/trading-mechanics/perps#margin-ladders).
* `updatedAt`: string
**Example — two-tier ladder:**
```typescript theme={null}
const config = await sdk.market.getPerpMarketConfig(pair.id);
// config.riskTiers ==
// [
// { tierLevel: 1, lowerBoundNotional: "0", maxLeverage: "50", initialMarginRatio: "0.02", maintenanceMarginRatio: "0.01", initialMarginDeduction: "0", maintenanceMarginDeduction: "0" },
// { tierLevel: 2, lowerBoundNotional: "100000000", maxLeverage: "25", initialMarginRatio: "0.04", maintenanceMarginRatio: "0.02", initialMarginDeduction: "2000000", maintenanceMarginDeduction: "1000000" },
// ]
// A $200M position falls in tier 2: MM = 200_000_000 * 0.02 - 1_000_000 = 3_000_000
```
Aggregate snapshot of a perp market: mark, index, 24h stats, open interest, current and estimated next funding rate.
**Returns — `PerpMarketSummary`:**
* `tradingPairId`, `symbol`: string
* `lastPrice`, `markPrice`, `indexPrice`: string
* `high24h?`, `low24h?`, `priceChange24h?`, `priceChangePercent24h?`: string
* `volume24h?`: string — 24h volume in base-asset units
* `quoteVolume24h?`: string — 24h volume in quote-token (USDC) units
* `openInterest`: string — open interest in base-asset units (alias of `openInterestBase`)
* `openInterestBase?`: string — open interest in base-asset units
* `openInterestNotional?`: string — open interest notional in quote (USDC) units, equal to `openInterestBase` × mark price (falling back to the latest trade/close price when no mark is available)
* `currentFundingRate?`, `estimatedNextFundingRate?`: string
* `nextFundingTime?`: string — ISO 8601
* `marketStatus`, `marketRegime`: string
* `updatedAt`: string
Live mark price (oracle reference used for PnL and liquidation).
**Returns:** `{ tradingPairId, markPrice, oracleProvider, oracleEpoch?, regime, updatedAt }`
Live index price (aggregated external fair-value reference used for funding).
**Returns:** `{ tradingPairId, indexPrice, components: IndexComponent[], updatedAt }`. Each `IndexComponent` is `{ provider, price, weight?, updatedAt? }`.
Current and estimated-next funding rate, plus next settlement time.
**Returns:** `{ tradingPairId, currentFundingRate, estimatedNextFundingRate?, nextFundingTime, fundingIntervalSeconds, lastFundingTime?, updatedAt }`
`nextFundingTime` reflects the market's actual settlement cadence, and `estimatedNextFundingRate` refreshes as new samples arrive.
Historical settled funding for a single pair.
**Parameters:** all optional
* `startTime?`, `endTime?`: string — ISO 8601 or ms-epoch string
* `pageSize?`: number — items per page, 1 to 1000 (default 50)
* `pageToken?`: string — pagination cursor: omitted or `""` starts a walk from the newest settlement (the oldest under `orderBy: "ASC"`); a previous `nextPageToken` resumes it. There is no `page`: the SDK rejects one with a `ValidationError` before sending, and a raw REST request carrying it gets `400`. The walk seeks on `(settledAt, id)`, so a page boundary inside one settlement instant no longer repeats or drops a row. The filters and `orderBy` must match the request that minted the token
* `orderBy?`: `"ASC" | "DESC"` — sort direction for `settledAt`, default `DESC`
**Returns:** `{ records: FundingRecord[], total, pageSize, nextPageToken, totalCapped }`. `nextPageToken` is `""` once the walk is exhausted — each page over-fetches one row, so an empty token is definitive; follow a non-empty one and do not drive the loop off `total`, which is bounded by the server count ceiling (`totalCapped` is `true` when more matched, making `total` a lower bound). The ergonomic `FundingRecord` surfaces `{ tradingPairId, fundingRate, cumulativeFundingPerUnit?, carried? }` — `carried` is `true` when the window settled at the carried last rate under the sparse-sample rule. The ergonomic type also carries the per-window fields — `epoch`, `fundingDeltaPerUnit`, `referencePrice` (the settlement mark price), `sampleCount`, `windowStartedAt`, `windowClosedAt`, and `settledAt` (the sort key for `listAllFundingHistory`). `fundingDeltaPerUnit` is normally `fundingRate × referencePrice`; in the one defensive case where that product cannot be represented (the engine bounds the rate below 100 % per window, so this needs a mark near the numeric ceiling), the record keeps the `fundingRate` that would have applied, settles `fundingDeltaPerUnit` as `0`, leaves `cumulativeFundingPerUnit` unchanged and still advances `epoch` — a window in which nothing was paid, not a missing window.
Settled funding history across **every** perp market in one call, so you no longer fan out one `listFundingHistory` request per pair. No authentication required.
**Parameters:** all optional
* `startTime?`, `endTime?`: string — ms-epoch strings, inclusive bounds on `settledAt`
* `pageSize?`: number — items per page, 1 to 1000 (default 50)
* `pageToken?`: string — pagination cursor: omitted or `""` starts a walk from the newest settlement (the oldest under `orderBy: "ASC"`); a previous `nextPageToken` resumes it. There is no `page`: the SDK rejects one with a `ValidationError` before sending, and a raw REST request carrying it gets `400`. The walk seeks on `(settledAt, id)`, so a page boundary inside one settlement instant no longer repeats or drops a row. The filters and `orderBy` must match the request that minted the token
* `orderBy?`: `"ASC" | "DESC"` — sort direction for `settledAt`, default `DESC`
**Returns:** `{ records: FundingRecord[], total, pageSize, nextPageToken, totalCapped }`. `nextPageToken` is `""` once the walk is exhausted — each page over-fetches one row, so an empty token is definitive; follow a non-empty one and do not drive the loop off `total`, which is bounded by the server count ceiling (`totalCapped` is `true` when more matched, making `total` a lower bound). Rows are sorted by the wire `settledAt` field. Each `FundingRecord` carries its own `tradingPairId`, so you can group rows by market client-side (see `listFundingHistory` for the full record shape).
**Example:**
```typescript theme={null}
const funding = await sdk.market.listAllFundingHistory({ orderBy: "DESC", pageSize: 100 });
funding.records.forEach((r) => {
console.log(`${r.tradingPairId}: ${r.fundingRate}${r.carried ? " (carried)" : ""}`);
});
```
Current open interest in base-asset units and notional.
**Returns:** `{ tradingPairId, openInterestBase, openInterestNotional, updatedAt }`
## Orderbook Snapshot
Get a point-in-time orderbook snapshot via REST API. For real-time updates, use WebSocket subscriptions.
Fetch current orderbook state for a trading pair
**Parameters:**
* `tradingPairId`: string - Trading pair UUID
* `options?`: Optional settings
* `depth?`: number - Number of price levels (default: 10, max: 100)
* `tradingMode?`: "SPOT" | "MARGIN" - Trading mode (default: "SPOT")
* `magnitude?`: number - Price grouping (0.0001, 0.001, 0.01, 0.1, 1, 10, 100, 1000, 10000)
* `denomination?`: "BASE" | "QUOTE" - Quantity denomination
**Returns:**
* `symbol`: string - Trading pair symbol
* `tradingMode`: "SPOT" | "MARGIN" - Trading mode
* `bids`: Array of `{ price, quantity, orderCount }`
* `asks`: Array of `{ price, quantity, orderCount }`
* `bestBid?`: string - Best bid price (optional)
* `bestAsk?`: string - Best ask price (optional)
* `bidVolume?`: string - Total bid volume (optional)
* `askVolume?`: string - Total ask volume (optional)
* `priceChange?`: object - Price change info (optional)
* `baseDecimals`: number - Base token decimals
* `quoteDecimals`: number - Quote token decimals
* `timestamp`: string - Snapshot timestamp (UTC)
* `sequence`: number - Sequence number
**Example:**
```typescript theme={null}
const pair = await sdk.market.getTradingPairBySymbol('BTC/USDC');
const orderbook = await sdk.orderbook.getOrderbook(pair.id, {
depth: 20,
magnitude: 1 // Group by $1 price levels
});
console.log(`Best bid: ${orderbook.bestBid}`);
console.log(`Best ask: ${orderbook.bestAsk}`);
console.log(`Spread: ${Number(orderbook.bestAsk) - Number(orderbook.bestBid)}`);
```
## Historical Trades
Fetch recent trade history for a trading pair. For real-time trade updates, use WebSocket subscriptions.
Fetch one page of historical trades for a trading pair (newest first)
**Cursor paged.** The page number was removed: `trades` is appended to
continuously, so an offset silently re-served the newest trades as fills
landed mid-walk. Omit `pageToken` to start, then follow `nextPageToken` until
it comes back empty. Sending `page` is rejected before the request leaves the
SDK, and the REST API answers `400` if it reaches the wire.
A walk reaches the end of the archiver's retention window, not the end of the
pair's history — the endpoint's long-standing scope. There is no public walk
over a pair's archived history, so do not treat
[`getUserTrades`](/sdk/typescript/profile) as a continuation of this one: it is
authenticated and returns only trades where **you** were maker or taker, so
following it to extend a public tape would silently drop every fill between two
other parties. Use it for your own archived fills. A single archived trade is
still publicly retrievable by id via `getTradeById`.
**Parameters:**
* `tradingPairId`: string - Trading pair UUID
* `options?`: Optional settings
* `pageSize?`: number - Max records to return (default: 25, max: 1000)
* `pageToken?`: string - Cursor from a previous response's `nextPageToken`; omit to start at the most recent trade
**Returns:** `{ trades, nextPageToken }`, where `nextPageToken` is empty
exactly when the walk is finished (the server over-fetches one row to prove
it, so no confirming extra request is needed), and `trades` is an array of
`TradeEvent`:
* `eventType`: "trade"
* `tradingPairId`: string - Trading pair UUID
* `tradingMode`: "SPOT" | "MARGIN"
* `data`:
* `tradeId`: string - Unique trade ID
* `price`: string - Execution price
* `quantity`: string - Executed quantity
* `makerSide`: "BUY" | "SELL" - Side of the maker order
* `executedAt`: string - ISO 8601 date-time timestamp (UTC), empty string if the trade has no recorded execution time
**Timestamp format:** `executedAt` is an ISO-8601 / RFC 3339 date-time string with an explicit `+00:00` UTC offset (e.g. `2025-01-15T10:30:00+00:00`). Any standard parser, including `new Date(...)`, handles it.
**Example:**
```typescript theme={null}
const pair = await sdk.market.getTradingPairBySymbol('BTC/USDC');
// Last 50 trades
const { trades } = await sdk.trades.getTrades(pair.id, { pageSize: 50 });
trades.forEach(trade => {
console.log(`${trade.data.makerSide} ${trade.data.quantity} @ ${trade.data.price}`);
});
// Walk the whole hot feed
let pageToken: string | undefined;
do {
const page = await sdk.trades.getTrades(pair.id, { pageSize: 1000, pageToken });
handle(page.trades);
pageToken = page.nextPageToken || undefined;
} while (pageToken);
```
Fetch a single trade by its UUID. Throws `APIError` if the trade is not found.
**Parameters:**
* `tradeId`: string - Trade UUID
**Returns:** A single `TradeEvent` (same shape as the entries returned by `getTrades`). `data.executedAt` is an empty string if the trade has no recorded execution time.
**Example:**
```typescript theme={null}
const trade = await sdk.trades.getTradeById('a1b2c3d4-...');
console.log(`${trade.data.makerSide} ${trade.data.quantity} @ ${trade.data.price}`);
```
# TraderCodes
Source: https://docs.0xmonaco.com/sdk/typescript/pitpass
Referral codes, rewards balance, and rewards transfer
```typescript theme={null}
import { MonacoSDK } from "@0xmonaco/core";
// Get the authenticated user's TraderCode (with custom-handle fields)
await sdk.pitpass.getMyTraderCode();
// Claim or edit a custom (vanity) handle
await sdk.pitpass.setTraderCode(code);
// Revert to the wallet-derived default
await sdk.pitpass.clearTraderCode();
// Advisory check of a handle's availability (public, no auth required)
await sdk.pitpass.checkTraderCodeAvailability(code);
// Validate a referral code (public, no auth required)
await sdk.pitpass.getTraderCodeInfo(code);
// Read accrued rewards
await sdk.pitpass.getRewardsBalance();
// Move rewards into a trading balance
await sdk.pitpass.transferRewards({ token, amount });
```
For a full integration walkthrough see [TraderCodes](/developers/tradercodes).
***
Return the authenticated caller's TraderCode. The default code is derived from the wallet
address and is created on demand — this method never returns `NOT_FOUND` for a signed-in user.
Requires authentication.
**Returns:** `TraderCodeResponse`:
* `code`: string — the wallet-derived code (normalized lowercase hex address)
* `createdAt`: string — ISO 8601 timestamp of when the code row was first derived
* `customCode`: string — the claimed custom handle as entered, or an empty string when the wallet-derived default is kept
* `isCustom`: boolean — whether a custom handle is currently set
* `display`: string — the string to show users (`Monaco - ` for the default, or the custom handle)
**Example:**
```typescript theme={null}
const { code, display, isCustom } = await sdk.pitpass.getMyTraderCode();
// Render `display` directly; embed `code` in a referral URL: ?ref=
console.log(display, isCustom);
```
Claim or edit the caller's custom (vanity) handle. The handle is stored exactly as entered
(casing preserved) but is unique case-insensitively; it must be letter-first, `[A-Za-z0-9_]`,
and 5–20 characters. The SDK validates the handle client-side before sending, but the server
remains the authoritative gate.
Requires authentication.
**Parameters:**
* `code`: string — the desired handle
**Returns:** `TraderCodeResponse` — the updated code, with `isCustom: true` and `customCode` set.
Throws `409` when the handle is already taken (case-insensitive) or is a recently-freed handle
still in cooldown, `422` on validation failure (the error carries a stable reason code), `429`
when handles are edited too frequently, and `403` when custom handles are disabled.
**Example:**
```typescript theme={null}
const { display, isCustom } = await sdk.pitpass.setTraderCode("acePilot");
console.log(display, isCustom); // "acePilot", true
```
Drop the caller's custom handle and revert to the wallet-derived default. Idempotent — clearing
when no handle is set simply returns the default code.
Requires authentication.
**Returns:** `TraderCodeResponse` — the code with `isCustom: false` and an empty `customCode`.
Clearing counts against the same rolling-hour edit limit as `setTraderCode`, so it throws `429`
when edited too frequently and `403` when custom handles are disabled.
**Example:**
```typescript theme={null}
const { display, isCustom } = await sdk.pitpass.clearTraderCode();
console.log(isCustom); // false
```
Public advisory check of whether a custom handle can be claimed. Use it for live feedback in a
claim form; it is not the authoritative gate — `setTraderCode` still validates on the server.
No authentication required. Rate-limited per client (throws `429` once the caller exceeds the
allowance).
**Parameters:**
* `code`: string — the handle to check
**Returns:** `TraderCodeAvailabilityResponse`:
* `available`: boolean — whether the handle can be claimed
* `reason?`: string — present only when unavailable; one of `taken`, `too_short`, `too_long`, `reserved`, `invalid_char`, or `looks_like_address`
**Example:**
```typescript theme={null}
const { available, reason } = await sdk.pitpass.checkTraderCodeAvailability("acePilot");
if (!available) {
console.log(`Unavailable: ${reason}`);
}
```
Public lookup for a TraderCode. Accepts a bare wallet address or the `Monaco - `
display form. Use this to validate a referral code before showing a confirmation to the user.
No authentication required.
**Parameters:**
* `code`: string — wallet address or `Monaco - ` display form
**Returns:** `TraderCodeInfoResponse`:
* `code`: string — normalized wallet address
Throws `404` when the code does not resolve to a known wallet.
**Example:**
```typescript theme={null}
try {
const info = await sdk.pitpass.getTraderCodeInfo("0xabc...");
console.log(info.code); // normalized address
} catch (err) {
// 404 — code is not recognized; show a friendly message
}
```
Return the caller's current rewards-bucket balances across all reward tokens. Rewards accrue
here as referred users trade. Returns an empty `balances` array if no rewards have been earned.
Requires authentication.
**Returns:** `GetRewardsBalanceResponse`:
* `balances`: `RewardsBalanceEntry[]`:
* `token`: string — reward token contract address (0x-prefixed)
* `available`: string — raw atomic units (divide by token decimals for display)
**Example:**
```typescript theme={null}
const { balances } = await sdk.pitpass.getRewardsBalance();
for (const entry of balances) {
const display = Number(entry.available) / 1e6; // USDC has 6 decimals
console.log(`${display} USDC available`);
}
```
Move earned rewards from the rewards bucket into the authenticated application's trading
balance. The transfer is ledger-only — no on-chain transaction, no gas. Funds are available
for trading immediately after the transfer.
Each call spends the per-user [movement budget](/developers/rate-limits) that withdrawals and
collateral transfers share, whatever the rewards balance: `429` with `retryAfter` when that
budget is enforced and spent.
Requires authentication.
**Parameters:** `TransferRewardsParams`:
* `token`: string — token contract address (0x-prefixed EVM address)
* `amount`: string — positive integer string in raw atomic units (e.g. `"1000000"` for 1 USDC)
**Returns:** `TransferRewardsResponse`:
* `rewardsBalance`: string — remaining balance in the rewards bucket after the transfer (raw atomic units)
* `tradingBalance`: string — new trading balance for this token after the transfer (raw atomic units)
Throws `409` when the requested `amount` exceeds the available rewards balance.
**Example:**
```typescript theme={null}
const { rewardsBalance, tradingBalance } = await sdk.pitpass.transferRewards({
token: "0xusdc...",
amount: "1000000", // 1 USDC in raw units
});
console.log(`Remaining rewards: ${rewardsBalance}`);
console.log(`New trading balance: ${tradingBalance}`);
```
Read the authenticated caller's own referral position. Self-scoped — the server derives your
identity from the session, so it only ever returns your own chain. Requires authentication.
**Returns:** `GetMyReferralPositionResponse`:
* `referrer`: string — the wallet address of your direct (L1) referrer, or `""` when you were not referred. Use the empty check to drive an "already referred" state.
* `upline`: array of `{ wallet, display, level }` — your referral chain upward, nearest first: L1 (your referrer), L2, and so on. At most as many entries as TraderCodes rewards levels (never more than 10); empty when you were not referred. These are the wallets your own trades pay.
* `referrerDisplay`: string — the referrer's custom TraderCode handle if they have claimed one, otherwise their wallet address (render this).
* `totalEarned`: string — all-time rewards you have earned, in raw atomic units.
* `rewards`: array of up to your 100 most recent reward rows (`totalEarned` remains the complete aggregate), each `{ source, sourceDisplay, level, amount, token, rateApplied, createdAt }` — `source`/`sourceDisplay` identify the trader whose fills paid you, and `level` is how far down your chain that trader sits: 1 for a direct referee, 2 for their referee, and so on (at most 10).
Throws `401` when unauthenticated, `429` when the account's read budget is exhausted (retry after the interval in the error detail), and `500` on a server error.
**Example:**
```typescript theme={null}
const { referrer, referrerDisplay, totalEarned } = await sdk.pitpass.getMyReferralPosition();
if (referrer) console.log(`Referred by ${referrerDisplay}`);
```
Read who you earn from and how much. Self-scoped, read-only. Requires authentication.
`earningsBySource` is paged. Called without `params`, it returns only the **first page** (up to 100 rows), not every row — follow `nextPageToken` until it is `""` to read the rest.
**Parameters:**
* `pageSize?`: number — `earningsBySource` rows per page, 1–500 (default 100).
* `pageToken?`: string — opaque `nextPageToken` from the previous response; omit for the first page.
**Returns:** `GetMyReferralDownlineResponse`:
* `earningsBySource`: array of `{ source, sourceDisplay, level, totalEarned, token }` — one page of rows, one per `(source, level, token)` across every reward level, ordered by each source account's total, largest first, so the same trader can appear more than once (a different level or token); `source` is not unique. Rows of a wallet that traded through several applications are merged within a page at its first part's position, so a merged row can sit below a smaller one, and that wallet can appear again on a later page.
* `directReferees`: array of `{ wallet, display, referredAt, totalEarned }` — your direct (L1) referees, including those who have not traded yet (`totalEarned: "0"`). Shows up to the 500 most recent effective referees (after wallet dedupe), drawn from a bounded scan, so a very large history may yield an incomplete roster; the earnings views stay complete.
* `summary`: `{ totalEarned, byLevel, level1Earned, level2Earned, level3Earned }` — your earnings broken down by level, covering every level and source regardless of the page. `byLevel` is an array of `{ level, earned }` (raw atomic units), ascending, one entry per level with earnings, including levels deeper than TraderCodes rewards today. `level1Earned`, `level2Earned`, and `level3Earned` are **deprecated** (levels 1–3 only) — use `byLevel`.
* `nextPageToken`: string — pass it back as `pageToken` for the next page; `""` when there are no more rows.
Throws a `ValidationError` before sending when `pageSize` is outside 1 to 500; `400` when `pageToken` is malformed; `401` when unauthenticated; `429` when the account's read budget is exhausted (retry after the interval in the error detail); `500` on a server error.
**Example:**
```typescript theme={null}
const first = await sdk.pitpass.getMyReferralDownline({ pageSize: 500 });
const rows = [...first.earningsBySource];
let pageToken = first.nextPageToken;
while (pageToken) {
const page = await sdk.pitpass.getMyReferralDownline({ pageSize: 500, pageToken });
rows.push(...page.earningsBySource);
pageToken = page.nextPageToken;
}
console.log(`Total earned: ${first.summary.totalEarned}, from ${rows.length} rows`);
for (const { level, earned } of first.summary.byLevel) console.log(`L${level}: ${earned}`);
```
Read one level of your referral tree. Self-scoped, read-only. Requires authentication.
**Parameters:**
* `parent?`: string — wallet whose direct referees to list. Omit for your own L1 referees; pass one of your referees above the deepest rewarded level to expand that node.
* `pageSize?`: number — nodes per page, 1–200 (default 50).
* `pageToken?`: string — `nextPageToken` from the previous page; only valid with the same `parent`.
**Returns:** `GetMyReferralTreeResponse`:
* `parent`: string — the wallet whose referees are listed (your own wallet when `parent` was omitted).
* `parentLevel`: number — `0` for you, `1`–`9` for an expanded referee.
* `nodes`: array of `{ wallet, display, level, parent, referredAt, refereeCount, totalEarned }`, newest first — `level` is the depth below you (1–10, down to the deepest level TraderCodes rewards), `refereeCount` is how many wallets that node referred (expand it when above 0; always 0 at the deepest rewarded level), and `totalEarned` is what you earned from that wallet's own trades in raw atomic units. Each wallet appears once, under the referrer that earns from it.
* `nextPageToken`: string — pass it back for the next page; `""` on the last page.
Throws `400` for an invalid `parent`, `pageSize`, or `pageToken`, `401` when unauthenticated, `404` when `parent` is not you or one of your own referees above the deepest rewarded level, `429` when the account's read budget is exhausted, and `500` on a server error.
**Example:**
```typescript theme={null}
const root = await sdk.pitpass.getMyReferralTree();
for (const node of root.nodes) {
if (node.refereeCount > 0) {
const children = await sdk.pitpass.getMyReferralTree({ parent: node.wallet });
console.log(`${node.display} referred ${children.nodes.length} wallets`);
}
}
```
# Portfolio Analytics
Source: https://docs.0xmonaco.com/sdk/typescript/portfolio
Authoritative portfolio summary, performance, realized PnL, assets, and margin
```typescript theme={null}
import { MonacoSDK } from "@0xmonaco/core";
// Initialize SDK with wallet client (see Initialization guide)
// Current equity split plus period activity
await sdk.portfolio.getSummary({ period: "1m" });
// Transfer-neutral dollar PnL and sampled time-weighted return
await sdk.portfolio.getPerformance({ period: "1w" });
// Realized PnL split into perp closes, spot trades, funding, and fees
await sdk.portfolio.getRealizedPnl({ period: "all" });
// Per-asset balances priced by asset ID
await sdk.portfolio.getAssets({ period: "1d" });
// Current margin totals and per-risk-bucket projections (no period)
await sdk.portfolio.getMargin();
await sdk.portfolio.getMargin({ marginAccountId, includeAccounts: true });
```
These methods return authoritative calculations computed in Core — the same numbers across every client. They are application-scoped and require authentication.
`getSummary`, `getPerformance`, `getRealizedPnl`, `getAssets`, and the two `sdk.profile` methods below read from a dedicated replica, so they **may reflect replica lag** and can trail a live position or balance read by a short interval. Do not reconcile a number from this page against a live one to the cent. A reader outage returns a retryable `503` (gRPC `Unavailable`) rather than falling back to the writer, so treat unavailability as transient and retry. `getMargin()` is the exception: it is writer-backed rather than replica-backed, as is trade history. Its withdrawable-collateral bounds are still 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 it serves bounds but one bucket is missing, only that row falls back. The reconstruction uses the same formulas the withdrawal gates enforce, but cannot see their 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. Do not make these analytics the sole input to a real-time control of your own — a withdrawal gate, a risk limit, or a liquidation decision; read the live surfaces for that.
Historical methods accept an optional `period` of `"1d"`, `"1w"`, `"1m"`, or `"all"`, defaulting to `all` when omitted. `getMargin()` is current-only and takes no period.
All decimal amounts and percentages are returned as strings. Every response carries a `window` (or `asOf`) describing the query cutoff and how much history the retained data actually covers. Dependent values that cannot be computed — a return over a window with no starting equity, or the value of an asset with no spot price — are **omitted** rather than reported as `0`; branch on their presence before displaying.
Two shared shapes appear across these responses:
* **`PortfolioWindow`** — `period`, `asOf` (query cutoff, ISO 8601), optional `dataThrough` / `requestedStartsAt` / `effectiveStartsAt` / `historyStartsAt`, and `coverage` (`"complete"`, `"partial"`, or `"none"`).
* **`PortfolioSeriesWindow`** — extends `PortfolioWindow` with `displayInterval` (`"5m"`/`"1h"`/`"4h"`/`"1d"`) and `calculationInterval` (`"1m"`/`"5m"`/`"15m"`/`"1h"`/`"4h"`/`"1d"`).
`valuationStatus` is `"complete"` when every input had a price and `"partial"` when at least one dependent value was omitted for a missing price.
***
Return the current equity breakdown plus trading activity over the requested window.
Requires authentication.
**Parameters:** `PortfolioPeriodParams` (optional):
* `period`: `"1d" | "1w" | "1m" | "all"` — defaults to `all`
**Returns:** `PortfolioSummary`:
* `window`: `PortfolioWindow`
* `valuationStatus`: `"complete" | "partial"`
* `current`: current equity split — `perpsEquity` (string), plus optional `totalEquity`, `spotEquity`, `unrealizedPnl`, and `availableValue`
* `activity`: period activity — `volume` (string), `tradeCount` (number), `orderCount` (number), `feesPaid` (string)
**Example:**
```typescript theme={null}
const summary = await sdk.portfolio.getSummary({ period: "1m" });
console.log(summary.current.totalEquity ?? "—");
console.log(`${summary.activity.tradeCount} trades, ${summary.activity.feesPaid} fees`);
```
Return transfer-neutral dollar PnL and a sampled time-weighted return (TWR) series for the
window. Deposits and withdrawals are excluded from PnL so external flows do not distort return.
For any period with a performance baseline, unpriceable external withdrawals are omitted from `withdrawals` and the dependent `deposits` total; successful responses with these omissions report `valuationStatus: "partial"`. A recognized quote asset without a valuation pair is valued at `1`, so it is not omitted for lacking a pair. Reconciliation checks still apply: if derived deposits become negative, the request fails as unavailable.
Requires authentication.
**Parameters:** `PortfolioPeriodParams` (optional):
* `period`: `"1d" | "1w" | "1m" | "all"` — defaults to `all`
**Returns:** `PortfolioPerformance`:
* `window`: `PortfolioSeriesWindow`
* `calculationMethod`: `"sampled_twr"`
* `valuationStatus`: `"complete" | "partial"`
* `startingEquity?`, `endingEquity?`: string
* `deposits`, `withdrawals`, `netExternalFlows`: string
* `pnl?`, `returnPct?`: string — omitted when the window lacks a starting equity to measure against
* `maxDrawdown?`: `{ amount, pct, peakAt, troughAt }` — positive loss magnitude, peak-to-trough sampled-TWR percentage, and the peak/trough timestamps
* `points`: `PortfolioPerformancePoint[]` — each `{ timestamp, equity, cumulativePnl, cumulativeReturnPct? }`
**Example:**
```typescript theme={null}
const perf = await sdk.portfolio.getPerformance({ period: "1w" });
console.log(perf.returnPct ?? "n/a", perf.maxDrawdown?.pct ?? "0");
```
Return realized PnL for the window, split by source. Spot-withdrawal disposal PnL is excluded.
Requires authentication.
**Parameters:** `PortfolioPeriodParams` (optional):
* `period`: `"1d" | "1w" | "1m" | "all"` — defaults to `all`
**Returns:** `PortfolioRealizedPnl`:
* `window`: `PortfolioSeriesWindow`
* `bucketInterval`: `"1h" | "1d"` — the series bucket size (hourly for `1d`, daily otherwise)
* `totals`: `PortfolioRealizedTotals` — `perpsClosedPnl`, `spotTradePnl`, `fundingPnl`, `feesPaid`, `netRealizedPnl` (all strings)
* `closedTrades`: `{ count, winners, losers, breakEven, positions, winRatePct? }` — `winners`, `losers`, `breakEven`, and `winRatePct` are **position-based**: each position (one flat→open→flat round-trip) is classified once by its net realized PnL after all fees, not per closing fill. `positions` is the total classified positions (`winners + losers + breakEven`); `count` stays fill-based (closing fills in the window)
* `bestDay?`: `{ date, netRealizedPnl }` — `date` is a UTC `YYYY-MM-DD` calendar day
* `points`: `PortfolioRealizedPnlPoint[]` — the `totals` fields plus a `timestamp`, one per bucket
**Example:**
```typescript theme={null}
const rp = await sdk.portfolio.getRealizedPnl({ period: "all" });
console.log(rp.totals.netRealizedPnl, rp.closedTrades.winRatePct ?? "—");
```
Return per-asset spot holdings priced by asset ID, with allocation percentages and window-scoped
spot performance. Rows for assets without a spot price are returned `unpriced`, with `value`,
`allocationPct`, and `markPrice` omitted.
Requires authentication.
**Parameters:** `PortfolioPeriodParams` (optional):
* `period`: `"1d" | "1w" | "1m" | "all"` — defaults to `all`
**Returns:** `PortfolioAssets`:
* `window`: `PortfolioSeriesWindow`
* `valuationStatus`: `"complete" | "partial"`
* `totalSpotEquity?`: string — omitted when any held asset is unpriced
* `performance`: `{ pnl?, returnPct?, calculationMethod: "sampled_twr" }`
* `assets`: `PortfolioAssetRow[]` — each `{ assetId, symbol, totalBalance, availableBalance, lockedBalance, markPrice?, value?, allocationPct?, priceAsOf?, pricingStatus, avgCost?, unrealizedPnl?, unrealizedRoe?, cumRealized? }` where `pricingStatus` is `"priced"` or `"unpriced"`. The spot cost-basis fields — `avgCost` (weighted-average cost per unit), `unrealizedPnl` (`costBasisQuantity × (markPrice − avgCost)`), `unrealizedRoe` (percentage return on cost basis), and `cumRealized` (lifetime realized PnL, gross of fees) — are present when the asset has a cost-basis checkpoint; `unrealizedPnl` and `unrealizedRoe` additionally require a mark price, and `unrealizedRoe` additionally requires a non-zero cost basis
**Example:**
```typescript theme={null}
const { assets, totalSpotEquity } = await sdk.portfolio.getAssets({ period: "1d" });
for (const a of assets) {
if (a.pricingStatus === "priced") console.log(a.symbol, a.value, a.allocationPct);
}
```
Return the current margin picture: parent-account totals and per-risk-bucket projections, kept
separate. Parent free collateral does not include risk-bucket free collateral. This method is
current-only and takes no period. By default it describes the default margin account; pass
`marginAccountId` to describe one of your sub-accounts instead, and `includeAccounts` to add the
owner rollup across every open margin account.
Requires authentication.
**Parameters:** `PortfolioMarginParams` (optional):
* `marginAccountId?`: string — one of your margin accounts (the default one or a sub-account) to describe instead of the default one; `total`, `riskBuckets` and the `positions` counts are then that account's own. Without it, `total` and `riskBuckets` describe the default margin account while the `positions` counts stay owner-wide across every margin account
* `includeAccounts?`: boolean — also return `accounts` and `ownerTotal`
**Returns:** `PortfolioMargin`:
* `asOf`: string — snapshot timestamp (ISO 8601)
* `marginAccountId?`: string — the margin account `total` and `riskBuckets` describe (and the `positions` counts too, when the request names `marginAccountId`; otherwise those stay owner-wide); absent before your first margin account exists
* `accounts?`: `PortfolioMarginAccount[]` — with `includeAccounts`: every open margin account, the default one first, then sub-accounts by key; each `{ marginAccountId, subaccountKey, total }`, where `subaccountKey` is `"default"` for the default margin account and `total` is a `PortfolioMarginTotal` whose `withdrawableCollateral` is the persisted reconstruction of the engine's bound
* `ownerTotal?`: `PortfolioMarginTotal` — with `includeAccounts`: the sum of `accounts`' totals
* `total`: `PortfolioMarginTotal` — `equity`, `collateralBalance`, `parentAvailableCollateral` (parent-only free collateral), `withdrawableCollateral`, `initialMarginRequired`, `maintenanceMarginRequired`, `unrealizedPnl` (all strings)
* `positions`: `{ total, cross, isolated }` — open-position counts
* `riskBuckets`: `PortfolioRiskBucket[]` — each `{ riskBucketId, marginMode, tradingPairId?, collateral, freeCollateral, withdrawableCollateral, maxRemovableCollateral?, availableOrderCollateral?, initialMarginRequired, maintenanceMarginRequired, unrealizedPnl, positionCount, copyFollowId?, copyLeaderHandle? }`. `availableOrderCollateral` is the engine-served capacity for **new** orders in that bucket (parent auto top-up included, not additive across rows — see the margin-accounts docs for the full semantics); it is present only on live-engine reads, so handle `undefined`. `maxRemovableCollateral` is the largest removal the engine accepts when it may also lower the posted margin of an isolated bucket's one open position (equal to `withdrawableCollateral` otherwise), also live-engine only. `marginMode` (`"ISOLATED"` / `"CROSS"`) is always present and lets you attribute bucket margin to cross or isolated without a second call; `tradingPairId` is present only on `ISOLATED` rows, since a cross bucket spans the pairs selected into it and is scoped to none individually (read that membership from the margin-accounts `selectedTradingPairIds`). `copyFollowId` and `copyLeaderHandle` are present only on a copy risk bucket row — the follow that funds it and the followed leader's custom TraderCode handle, when still held — and omitted on every other row.
`marginMode` does **not** reconcile with the `positions.cross` / `positions.isolated` counts, and grouping `riskBuckets` by mode will not reproduce them. Without `marginAccountId`, those counts are computed by a separate query over **every** margin account for your user/application pair, while `riskBuckets` comes from the single oldest (parent) account, and they attribute a legacy position carrying no risk-bucket reference to cross whether or not a cross bucket row exists. Treat them as account-wide totals, not as a checksum over the rows. For the same reason these labels are not a join key against the margin-accounts surface — that surface emits rows this one has no counterpart for, and the two disagree on legacy-position attribution on persisted-fallback reads.
`parentAvailableCollateral` is an **equity** figure: it credits settled realized PnL, accrued
funding, and unrealized gains, none of which are principal a transfer can move. The bound the
engine actually enforces on transfers-out and withdrawals — and, because allocating collateral to
a risk bucket passes the same principal-gated door, the effective ceiling on funding a new
isolated or first-use position — is `total.withdrawableCollateral`. The two can differ without
limit (e.g. a large banked realized PnL lifts `parentAvailableCollateral` far above what any
transfer would accept). Size transfers, withdrawals, and new isolated/first-use positions from
`withdrawableCollateral`; treat `parentAvailableCollateral`/`freeCollateral` as trading-margin
headroom for existing cross exposure.
**Example:**
```typescript theme={null}
const margin = await sdk.portfolio.getMargin();
console.log(margin.total.withdrawableCollateral); // what can actually move
for (const b of margin.riskBuckets) console.log(b.riskBucketId, b.withdrawableCollateral);
// One sub-account's own view, plus the rollup across every open margin account:
const rollup = await sdk.portfolio.getMargin({ marginAccountId: hedgeMarginAccountId, includeAccounts: true });
for (const a of rollup.accounts ?? []) console.log(a.subaccountKey, a.total.equity);
console.log(rollup.ownerTotal?.equity);
```
**Throws:** `400` when `marginAccountId` is not a UUID; `404` when it is not one of your margin
accounts; `401` without authentication; `429` past the read budget; `503` when portfolio data is
unavailable; `500` on an internal error.
## Aggregate stats and chart (`sdk.profile`)
Two earlier portfolio methods live under the `sdk.profile` namespace. They return a single aggregate snapshot and a simple time-bucketed series, and remain supported. Prefer the authoritative `sdk.portfolio` methods above for richer breakdowns and explicit history coverage. The v1.0.21 fee-total corrections apply to both methods below.
```typescript theme={null}
// Aggregate stats over a period
await sdk.profile.getPortfolioStats("30d");
// Time-bucketed chart series for a metric
await sdk.profile.getPortfolioChart("7d", "pnl");
```
Fetch aggregate trading statistics for the authenticated user over a given time period.
**Parameters:**
* `period`: `"24h"` | `"7d"` | `"30d"` | `"all"`
**Returns:**
* `pnl`: Average-cost realized PnL over the period (quote asset, normalized) — net of fees, across spot and perps incl. funding; excludes unrealized PnL. Spot withdrawals are booked as fair-value disposals `(last trade price − avg cost) × qty`; deposits and withdrawals are priced at the last observed trade price in event order; transfers that predate any trade on the pair are PnL-neutral; short-entry fees reduce short profits
* `volume`: Total trading volume (quote asset, normalized)
* `totalTrades`: Total number of trades executed
* `totalOrders`: Total number of orders placed
* `feesPaid`: Total fees paid (quote asset, normalized)
* `totalEquity`: Total equity (quote asset, normalized) — spot balances plus perpetuals (margin) account equity
* `spotEquity`: Spot account equity (quote asset, normalized)
* `perpsEquity`: Perpetuals (margin) account equity (quote asset, normalized)
* `winLossRatio`: Position-level win rate — winning positions over winning + losing positions, break-even excluded (`number`); each position (one flat→open→flat round-trip) is classified once by its net realized PnL after all fees, not per fill; `null` when the period has no closed positions
* `maxDrawdown`: Peak-to-trough drawdown on running PnL; `null` when the period has no closed trades
Fetch a time-bucketed chart series for a given period and metric.
**Parameters:**
* `period`: `"24h"` | `"7d"` | `"30d"` | `"all"`
* `metric`: `"volume"` | `"pnl"`
**Returns:**
* `metric`: The metric returned
* `period`: The period requested
* `data`: Array of `{ timestamp: string, value: string }` data points (ISO 8601 bucket start times)
# Positions
Source: https://docs.0xmonaco.com/sdk/typescript/positions
Inspect, modify, and close perp positions
```typescript theme={null}
import { MonacoSDK } from "@0xmonaco/core";
// Initialize SDK with wallet client (see Initialization guide)
// Read
await sdk.positions.listPositions(params?);
await sdk.positions.getPosition(positionId);
await sdk.positions.getPositionRisk(positionId);
await sdk.positions.listPositionHistory(params?);
// Modify
await sdk.positions.attachPositionTpSl(positionId, { takeProfit?, stopLoss?, trailingStop?, oco? });
// Close
await sdk.positions.closePosition(positionId, { closeType, ... });
await sdk.positions.batchCloseAllPositions(request?);
```
Positions are created by placing perp orders through `sdk.trading.placeLimitOrder` / `placeMarketOrder` with `tradingMode: "MARGIN"` and `leverage` — `side` sets the direction (one-way), and there is no `createPosition` call.
For lifecycle, prices, and UI patterns, see [Positions](/developers/positions).
***
## Read
Paginated list of positions, optionally scoped by margin account, trading pair, or status.
**Parameters:**
* `params?`: `ListPositionsParams`
* `marginAccountId?`: string
* `tradingPairId?`: string
* `status?`: `PositionStatusFilter` — `"OPEN" | "CLOSED" | "LIQUIDATING" | "LIQUIDATED"`; an `EXPIRED` position matches under a `CLOSED` filter
* `pageSize?`: number — 1 to 1000, defaults to 20
* `pageToken?`: string — cursor pagination. Omitted or `""` starts the walk (newest-closed first for `status: "CLOSED"` and `status: "LIQUIDATED"`; `(marginAccountId, tradingPairId, positionId)` order for `status: "OPEN"`; newest-opened first otherwise); otherwise pass the previous response's `nextPageToken`. There is no `page`: the SDK rejects one with a `ValidationError` before sending, and a raw REST request carrying it gets `400`. The filters must match the request that minted the token.
**Returns:** `ListPositionsResponse`:
* `positions`: `Position[]` — **order depends on the filter.** `status: "OPEN"` is served from the matching engine's live view, which holds no timestamp for a position the persistor has not written yet, so it is ordered by `(marginAccountId, tradingPairId, positionId)` ascending: stable and total, but UUID order rather than newest-first. `status: "CLOSED"` and `status: "LIQUIDATED"` read persisted rows ordered newest-closed first, on `(closedAt, id)`, so a position held for weeks and closed today leads the list (a legacy terminal row with no close time sorts by its open time). Every other filter reads persisted rows ordered newest-opened first, on the immutable `(openedAt, id)` key. Each is a separate cursor space, so a token is only valid in the one that minted it, and crossing between the live and persisted walks is **asymmetric**: a persisted token offered to `status: "OPEN"` while the engine is reachable is served by the persisted walk (so a walk that started on persisted rows during an outage keeps going once the engine returns), but a **live token cannot cross** — once the engine is unreachable the request is a `400` and the walk must restart, because the two relations share no sort key and resuming at a key from the wrong one would skip or repeat rows silently. A token neither recognises is a `400`
* `total`: number — bounded by the server count ceiling; a lower bound when `totalCapped` is `true`
* `pageSize`: number
* `nextPageToken`: string — cursor for the next page, `""` once the walk is exhausted. Each page over-fetches one row, so an empty token is definitive: follow a non-empty token, stop on an empty one
* `totalCapped`: boolean — `true` when more than the ceiling matched, so `total` is a lower bound; `false` when `total` is exact, including exactly the ceiling
**Throws:** REST `503` / gRPC `Unavailable` (code `SERVICE_UNAVAILABLE`) when the matching engine's index-price read fails transiently; retry. Any other failure of that read is `500`.
Full snapshot of a single position.
**Parameters:**
* `positionId`: string — UUID
**Returns:** `Position`:
* `positionId`: string
* `marginAccountId`: string
* `riskBucketId?`: string
* `marginMode?`: `"ISOLATED" | "CROSS"`
* `tradingPairId`: string
* `side`: `"LONG" | "SHORT" | "NONE"`
* `size`: string — base-asset quantity. On an open position this is the current absolute quantity; on a terminal (closed/liquidated/expired) row it is the total quantity closed over the position's life
* `entryPrice`: string
* `markPrice`: string
* `indexPrice?`: string
* `unrealizedPnl`: string
* `realizedPnl`: string
* `isolatedMargin`: string — on an open ISOLATED position, its committed position principal adjusted by settled realized PnL, fees, and funding, clamped at zero. Unfilled-order collateral is excluded. This is the unrealized-return denominator and the same backing used by isolated liquidation; live and persisted reads agree. A price gap can pass the backing before asynchronous liquidation occurs. On an open CROSS position, the posted margin. On a terminal row, the total initial margin allocated across reducing executions, used for lifetime realized return.
* `leverage?`: string
* `maintenanceMarginRequired`: string - position-scoped requirement at the mark price using the market maintenance-margin rate; `"0"` when the position has no open exposure
* `initialMarginRequired?`: string - position-scoped requirement at the mark price that honors effective leverage and the market initial-margin floor; `"0"` when the position has no open exposure
* `liquidationPrice`: string — isolated position/risk-bucket threshold; cross conditional per-position threshold, not a whole-account scalar
* `status`: `"OPEN" | "CLOSED" | "LIQUIDATING" | "LIQUIDATED" | "EXPIRED"` — `LIQUIDATED` and `EXPIRED` are terminal statuses in the exported `PositionStatus` union
* `netRealizedPnl?`: string — terminal rows only; lifetime realized PnL minus funding and whole-life `cumFees`, deducted once. Absent until the final close is recorded in its PnL sample and the reducing history is complete; poll again while pending. With no PnL sample it remains absent for 28 days after close, then falls back to the closing-execution net without unavailable opening fees. Also absent when reducing-history totals disagree with stored realized PnL.
* `realizedRoe?`: string — terminal rows only; `netRealizedPnl` as a percentage of the total allocated initial margin (leverage-aware realized return). Absent under the same condition as `netRealizedPnl`
* `exitPrice?`: string — terminal rows only; volume-weighted average price across the position's reducing executions. A position closed in several fills exits at this price, not at the final fill or the mark at close. Absent when the reducing-execution history is unavailable or fails its completeness check against the stored realized PnL; unlike `netRealizedPnl`, it does not wait for the final close to be recorded in the PnL sample
* `fundingPaid?`: string — terminal rows only; lifetime funding paid (signed; negative means received), summed over the position's reducing executions. Absent when the reducing-execution history is unavailable or fails its completeness check against the stored realized PnL; unlike `netRealizedPnl`, it does not wait for the final close to be recorded in the PnL sample
* `feesPaid?`: string — terminal rows only; trading fees paid on the position's reducing executions (signed; negative means rebated). Closing-side fees only — fees on opening fills are not included. Absent when the reducing-execution history is unavailable or fails its completeness check against the stored realized PnL; unlike `netRealizedPnl`, it does not wait for the final close to be recorded in the PnL sample
* `cumFees?`: string — total lifetime trading fees, including opening and reducing fills, application fees, liquidation fees and signed rebates (negative means net rebated). Present on open and terminal rows; serialized as `null` when the fee data is unavailable or incomplete, and after close until the final PnL sample arrives; on an `OPEN` or `LIQUIDATING` position it is also `null` when its reductions do not account for its `realizedPnl` (`lifetimeTotalsStatus` INCOMPLETE), rather than understated. Existing historical attribution errors are not repaired retroactively.
* `lifetimeTotalsStatus?`: `LifetimeTotalsStatus` | null — whether the lifetime totals (`exitPrice`, `realizedRoe`, `feesPaid`, `fundingPaid`, `cumFees`, `netRealizedPnl`) are served. `"LIFETIME_TOTALS_STATUS_COMPLETE"`: history covers the whole lifetime and the fields follow their own rules above. `"LIFETIME_TOTALS_STATUS_PENDING"`: history is still catching up to this row's `version`, or the row changed while it was being read; every lifetime field is absent — call again after a short backoff (about 1 second, growing to a few). `"LIFETIME_TOTALS_STATUS_INCOMPLETE"`: the lifetime totals are not complete now and a quick retry will not change that — a legacy-history gap (or reductions that do not reconcile) is permanent, while a missing or drifted maintained-totals row is repaired by the background reconcile (at startup and daily), after which the row reads COMPLETE; every field that is present comes from the maintained totals, which are cross-checked against each other and repaired by the background reconcile, e.g. `exitPrice` with no `cumFees`; `OPEN` and `LIQUIDATING` positions are checked against `realizedPnl` too and are INCOMPLETE with no `cumFees` when their reductions fall short. Until the position-history checkpoint is seeded, PENDING arises only when the position row changed between the row read and the totals read. Never read an absent total as zero. Typed with a `string` fallback so a newer status passes through; `null` or absent from an older server. See [Lifetime totals status](/developers/positions#lifetime-totals-status)
* `updatedAt`: string — ISO 8601, and **when the position was last mutated** (a fill, collateral transfer, funding settlement or close), not when the response was built. Polling an unchanged position returns the same timestamp on every surface — list, detail, risk, and the WebSocket `positions` channel alike — and an oracle reprice does not move it. That holds for an **open** position served from a reachable matching engine; a terminal row, a position read after a matching-engine restart, and an open row read while the engine is unreachable are served from other clocks — see [Inspect a Position](/developers/positions#inspect-a-position) and the [`positions` channel](/sdk/typescript/websockets#positions-channel). Display metadata: order by `version`, never by this
* `openedAt?`: string | null — ISO 8601, **when the position opened**: the fill that took its exposure from zero to non-zero. It never changes for the life of `positionId`, so time held is `now - openedAt` while open and `updatedAt - openedAt` once terminal. A flip opens a new position with its own `openedAt`. `null` (or absent, from an older server) when the opening instant is unknown — never filled with the read time, so check for both before date arithmetic
* `version?`: number — opaque, non-negative durable revision of this position row. Compare only within the same position; higher is newer, and never order by `updatedAt`. The WebSocket `positions` snapshot and **ranked** live `position_update` frames carry the same value for identical state, so reconcile on `positionId + version`. Some live frames are deliberately unranked and omit `version` even when their state matches this row — see [WebSockets](/sdk/typescript/websockets#positions-channel). The merge rule is **per field group**: on a higher version apply everything; on an equal version skip the versioned fields as an idempotent redelivery but still take `markPrice`, `unrealizedPnl`, `liquidationPrice`, `leverage`, `maintenanceMarginRequired` and `initialMarginRequired` from the newer payload, because those six are re-derived on every read rather than served from the versioned row and carry no version (five of them move with the oracle mark; `leverage` does not, but the live surfaces derive it while the persisted row serves its stored column, so it can still disagree at an unchanged version), and still take `isolatedMargin` too, because the WebSocket positions snapshot overlays it from an independent live matching-engine read rather than from the versioned row and it can therefore reflect a later command than the version names; on a lower version skip both groups, since an older command carries nothing newer — not even its mark. Discarding a whole payload on an equal version freezes displayed PnL until the position's next real mutation. `0` is the unranked sentinel and never participates in ordering — treat it exactly as an absent version and fall back to a full field comparison. It advances on sequenced mutations (open, add, reduce, close, funding, margin transfer, liquidation, ADL). A valuation-only oracle reprice persists nothing and advances no version; a state-transitioning oracle command (a maintenance-margin breach or recovery) persists the account's snapshot and restamps the positions it changed. It advances exactly when the row's versioned content changed at that sequence (a change undone within one persistence batch included); a position a command left unchanged keeps its version, so a ranked live frame for an untouched sibling of a filled position can carry a higher version than this row while describing identical content — rank it as newer, and nothing changes. A read served from live matching-engine state reports `0`: the engine holds the state but not the version, so it cannot rank itself — see [WebSockets](/sdk/typescript/websockets#positions-channel) for which frames are ranked
Non-zero funding frames carry this version from captured command state. Terminal close/liquidation/ADL frames normally carry it too and retain lifetime reduced quantity — the sum of every reducing execution — and the exact `CLOSED`/`LIQUIDATED` classification. After restart, a terminal frame rebuilt from pre-upgrade reduction history omits `version` when that history lacks the size or margin fields needed to reconstruct complete lifetime values; incomplete size history carries only the known partial total. Reconcile that frame by full field comparison. Detect closure from `status`, never from `size === "0"`.
**Throws:** REST `503` / gRPC `Unavailable` (code `SERVICE_UNAVAILABLE`) when the matching engine's index-price read fails transiently; retry. Any other failure of that read is `500`.
For `marginMode: "CROSS"`, a position's `liquidationPrice` varies only that position's mark while all other marks in the cross risk bucket remain unchanged. Other position marks, funding, realized PnL, fees/reserves, and collateral can change it. Render cross values as conditional. Treat an absent or blank value as unavailable, never `0`.
Just the volatile risk fields, refreshed. Cheaper than `getPosition` for live UI.
**Parameters:**
* `positionId`: string
**Returns:** `PositionRisk`:
* `positionId`: string
* `markPrice`: string
* `indexPrice?`: string
* `unrealizedPnl`: string
* `liquidationPrice`: string — isolated position/risk-bucket threshold; cross conditional per-position threshold, not a whole-account scalar
* `marginRatio`: string — account maintenance requirement divided by equity. At zero or negative equity with a remaining maintenance requirement it is a **maximum-distress sentinel** (`Decimal::MAX`, `79228162514264337593543950335`) rather than `0` or a negative value — clamp it for display, never render it raw as a percentage
* `maintenanceMarginRequired`: string - position-scoped requirement at the mark price using the market maintenance-margin rate; `"0"` when the position has no open exposure
* `initialMarginRequired?`: string - position-scoped requirement at the mark price that honors effective leverage and the market initial-margin floor; `"0"` when the position has no open exposure
* `updatedAt`: string — ISO 8601; the same mutation instant the matching position reports
**Throws:** REST `503` / gRPC `Unavailable` (code `SERVICE_UNAVAILABLE`) when the matching engine's index-price read fails transiently; retry. Any other failure of that read is `500`.
**Example:**
```typescript theme={null}
// Live liquidation-distance display, polled every 2 seconds
const position = await sdk.positions.getPosition(positionId);
setInterval(async () => {
const risk = await sdk.positions.getPositionRisk(positionId);
const liquidationPrice = risk.liquidationPrice?.trim();
if (!liquidationPrice) {
setLiquidationDisplay("unavailable");
return;
}
const mark = parseFloat(risk.markPrice);
const liq = parseFloat(liquidationPrice);
const distance = (mark - liq) / mark; // for a LONG
setLiquidationDisplay(
position.marginMode === "CROSS"
? `Conditional liquidation price: ${liquidationPrice}`
: liquidationPrice,
);
setDistance(distance);
}, 2000);
```
`PositionRisk` does not include `marginMode`; use the matching position or risk-bucket context to label a cross value as conditional rather than account-wide.
Lifecycle events for one position or all positions in scope: opens, partial fills, funding, margin changes, liquidations, closes.
**Parameters:**
* `params?`: `ListPositionHistoryParams`
* `positionId?`: string
* `marginAccountId?`: string
* `tradingPairId?`: string
* `reductionOnly?`: boolean — when `true`, returns only reductions, closes, and liquidations; omits opens, partial fills, funding, and margin-change events
* `pageSize?`: number — 1 to 1000, defaults to 20
* `pageToken?`: string — cursor pagination. Omitted or `""` starts the walk at the newest row; otherwise pass the previous response's `nextPageToken`. There is no `page`: the SDK rejects one with a `ValidationError` before sending, and a raw REST request carrying it gets `400`. The filters must match the request that minted the token.
**Returns:** `ListPositionHistoryResponse`:
* `events`: `PositionHistoryEvent[]` — each with `eventId`, `eventType`, optional `quantity` / `price` / `realizedPnl`, and `timestamp`. Reduction rows additionally include:
* `entryPrice?`: string — position's average entry price at close
* `allocatedInitialMargin?`: string — initial margin committed when the position was opened
* `fundingPaid?`: string — cumulative funding charges over the position's life
* `netRealizedPnl?`: string — realized PnL net of fees and funding
* `realizedRoe?`: string — return on equity (net PnL ÷ allocated initial margin)
* `positionSide?`: `"LONG" | "SHORT"` — side of the closed position
* `pageSize`: number
* `nextPageToken`: string — cursor for the next page, `""` once the walk is exhausted. Each page over-fetches one row, so an empty token is definitive: follow a non-empty token, stop on an empty one
No item count: this listing spans the hot table and the archive, and counting behind a filter would walk the wallet's whole lifetime prefix on every page.
***
## Modify
The positions API has no margin-adjustment method. Adjust a position's
margin through `sdk.marginAccounts` — see
[Margin Accounts](/sdk/typescript/margin-accounts). Every leg moves collateral
between a risk bucket and the **parent margin account**, never to or from the
spot wallet:
* **Add margin** — `transferCollateralToRiskBucket({ asset, amount, tradingPairId, marginMode: "ISOLATED" })`. By default (or with `applyToPositionMargin: true`) the amount is committed to the open position's stored margin — leverage drops and stays down. Pass `applyToPositionMargin: false` to fund the bucket with spendable free collateral instead (what order-funding loops need). (The deprecated pair-scoped `transferCollateralToMarginAccount` matches the default.)
* **Release collateral back to the parent** — `transferCollateralFromMarginAccount(parentMarginAccountId, { asset, amount, tradingPairId })`.
Set take-profit, stop-loss and/or trailing-stop conditional orders bound to the position. Every leg is optional; at least one is required.
**Parameters:**
* `positionId`: string
* `request`: `AttachPositionTpSlRequest`
* `takeProfit?`: `TpSlLeg`
* `stopLoss?`: `TpSlLeg`
* `trailingStop?`: `TrailingStopLeg` — one per position (a second attach returns `409`); never part of `oco`
* `oco?`: boolean — cancel the sibling leg after one triggers. Requires both `takeProfit` and `stopLoss`
**`TpSlLeg`:**
* `triggerPrice`: string
* `orderType`: `"LIMIT" | "MARKET"`
* `limitPrice?`: string — required for LIMIT
* `closePosition?`: boolean — resolve the full live position size when the leg triggers
* `quantity?`: string — fixed size; defaults to full position size when omitted. Required when `closePosition` is `false`; must be omitted when `closePosition` is `true`
* `timeInForce?`: `"GTC" | "IOC"`
* `slippageToleranceBps?`: number
* `expiresAt?`: string — ISO 8601, auto-cancel if not triggered
**`TrailingStopLeg`** (reduce-only MARKET close whose trigger trails the best mark since arming — see [Trailing Stop](/developers/trade#trailing-stop)):
* `trailBps`: number — integer, 10 to 2000 basis points behind the watermark
* `activationPrice?`: string — mark level that starts tracking and seeds the watermark; rejected if already reached. Omitted, tracking starts at the current mark
* `closePosition?` / `quantity?` — same sizing contract as `TpSlLeg`
* `slippageToleranceBps?`: number — 0 to 10000
* `expiresAt?`: string — ISO 8601
**Returns:** `AttachPositionTpSlResponse`:
* `positionId`: string
* `takeProfitOrderId?`: string
* `stopLossOrderId?`: string
* `trailingStopOrderId?`: string
* `status`: `"SUCCESS" | "FAILED"`
* `message`: string
**Throws:**
* REST `409` / gRPC `AlreadyExists` when `trailingStop` is set and the position already has an active trailing stop (or a resting entry order on the same trading pair and risk bucket carries one that would arm on its fill)
* REST `429` / gRPC `ResourceExhausted` when the order-creation budget is spent — each attach draws **one** item from the budget it shares with order creates, whatever legs it carries. See [Rate Limits](/developers/rate-limits)
**Example:**
```typescript theme={null}
await sdk.positions.attachPositionTpSl(positionId, {
takeProfit: { triggerPrice: "2200", orderType: "MARKET" },
stopLoss: {
triggerPrice: "1900",
orderType: "LIMIT",
limitPrice: "1895",
},
});
// Full-position OCO bracket: whichever leg triggers closes the whole
// position and cancels its sibling.
await sdk.positions.attachPositionTpSl(positionId, {
takeProfit: { triggerPrice: "2200", orderType: "MARKET", closePosition: true },
stopLoss: { triggerPrice: "1900", orderType: "MARKET", closePosition: true },
oco: true,
});
```
Attaching a full-close (`closePosition: true`) leg **supersedes** the position's previously active full-close legs of the same condition type: the old legs are cancelled atomically in the same engine step that creates the replacement — each emitting a `conditional_order_update` with `reason: "cancelled"` before the replacement's `"created"` — so moving a TP or SL is one call, with no window in which the position holds both levels or neither. OCO siblings, fixed-`quantity` legs (laddered partial exits still stack), entry-order-attached legs and trailing stops survive a supersede. A trailing stop never supersedes: a second one on the same position is rejected with `409`. See [Order Management](/developers/trade#conditional-perps-orders).
Lifecycle for these conditionals streams via the [`conditional_order_update`](/sdk/typescript/websockets) websocket event.
Resident conditional-order caps default to 10 active legs per position, 100 resident legs per user on an engine shard (including pending entry-order legs), and 100,000 on that shard; deployments can override them. The attach subtracts full-close legs it supersedes before checking the caps. An engine cap refusal creates none of its new legs, but this adapter reports it as generic `500` `MATCHING_ENGINE_ERROR` / gRPC `INTERNAL`, not a structured cardinality error. That status alone does not identify the cause. See [Conditional Perps Orders](/developers/trade#conditional-perps-orders).
**Throws:** `400` for handler-validated invalid legs or a position that is not open; `401` without valid authentication; `403` for a delegated session or denied access; `404` for a missing or unowned position; `409` for an existing or pending trailing stop; `429` when the order-creation budget is enforced and exhausted; `500` on an engine cap refusal or internal failure; `503` for an unavailable risk bucket, transient engine unavailability or maintenance. A known cardinality refusal needs fewer resident legs, not a timed retry.
***
## Close
Submit a closing order against the position. The position transitions to `CLOSED` only after the closing order fills.
**Parameters:**
* `positionId`: string
* `request`: `ClosePositionRequest`
* `closeType`: `"MARKET" | "LIMIT" | "IOC"`
* `limitPrice?`: string — required for `LIMIT` and `IOC`, forbidden only for `MARKET`
* `quantity?`: string — defaults to full position size (use for partial close)
* `slippageToleranceBps?`: number — for `MARKET` / `IOC`
**Throws:** REST `429` / gRPC `ResourceExhausted` when the risk-reduction order budget is enforced and spent — each close draws **one** item from the budget it shares with cancels, which is twice the order-creation budget and separate from it. Nothing was submitted; retry after `retryAfter`. See [Rate Limits](/developers/rate-limits)
**Returns:** `ClosePositionResponse`:
* `closeOrderId`: string
* `status`: string
* `message`: string
* `submittedQuantity`: string
**Examples:**
```typescript theme={null}
// Full market close
await sdk.positions.closePosition(positionId, { closeType: "MARKET" });
// Partial limit close
await sdk.positions.closePosition(positionId, {
closeType: "LIMIT",
limitPrice: "2050.00",
quantity: "0.5",
});
// IOC close with 0.5% slippage cap. An IOC close executes as a LIMIT+IOC
// order, so limitPrice is required.
await sdk.positions.closePosition(positionId, {
closeType: "IOC",
limitPrice: "2050.00",
slippageToleranceBps: 50,
});
```
Submit a MARKET reduce-only close for every open perp position owned by the caller — a one-call "panic close". Best-effort and partial: each position is closed independently, so a per-position failure is reported in `results` rather than aborting the batch.
**Parameters:**
* `request?`: `BatchCloseAllRequest`
* `tradingPairId?`: string — limit the close to one trading pair; if omitted, all open positions are closed
* `slippageToleranceBps?`: number — slippage tolerance applied to each MARKET close
**Throws:** REST `429` / gRPC `ResourceExhausted` when the risk-reduction order budget is enforced and spent — the call draws one item per open position it closes (an empty call draws one). The first 100 are charged before anything is sent: over budget the whole call is refused and nothing is closed; retry after `retryAfter`. Past 100 open positions, those the budget does not cover are not sent and come back in `results` with `RATE_LIMIT_EXCEEDED`. See [Rate Limits](/developers/rate-limits)
**Returns:** `BatchCloseAllResponse`:
* `totalRequested`: number — positions the batch attempted to close
* `totalClosed`: number — closes accepted by the matching engine (`SUCCESS`, `PARTIAL`, or `PENDING`)
* `totalFailed`: number
* `results`: `BatchCloseResult[]`
**`BatchCloseResult`:**
* `positionId`: string
* `closeOrderId?`: string — present when accepted
* `status?`: string — `"SUCCESS" | "PARTIAL" | "PENDING"`, present when accepted
* `submittedQuantity?`: string — present when accepted
* `error?`: `BatchCloseError` — present on failure
**`BatchCloseError`:**
* `code`: string — e.g. `POSITION_NOT_FOUND`, `INVALID_REQUEST`, `MATCHING_ENGINE_ERROR`
* `message`: string
**Examples:**
```typescript theme={null}
// Close every open position
const result = await sdk.positions.batchCloseAllPositions();
// Or limit to one pair with slippage tolerance
await sdk.positions.batchCloseAllPositions({
tradingPairId,
slippageToleranceBps: 50,
});
```
***
## Errors
* `400` on order placement and modify endpoints — see `rejectReason` returned by the matching engine
* `400` on `closePosition` when `quantity` exceeds remaining position size
* `400` on `attachPositionTpSl` when trigger prices are on the wrong side of the position (e.g., TP below entry on a LONG)
* `404` when the `positionId` does not exist
# Profile
Source: https://docs.0xmonaco.com/sdk/typescript/profile
User profile and account information
```typescript theme={null}
import { MonacoSDK } from "@0xmonaco/core";
// Initialize SDK with wallet client (see Initialization guide)
// Profile data
await sdk.profile.getProfile();
// Get the first page of balances (cursor paged: follow nextPageToken)
await sdk.profile.getUserBalances({ pageSize: 20 });
// Get balance for a specific asset (by UUID)
await sdk.profile.getUserBalanceByAssetId('123e4567-e89b-12d3-a456-426614174000');
// Transaction history with optional filters
await sdk.profile.getPaginatedUserMovements({ pageSize: 20 });
await sdk.profile.getPaginatedUserMovements({ entryType: "CREDIT", transactionType: "DEPOSIT" });
await sdk.profile.getPaginatedUserMovements({ assetId: "123e4567-e89b-12d3-a456-426614174000" });
// Trade history (cursor-paginated by default; follow nextPageToken)
await sdk.profile.getUserTrades({ pageSize: 20 });
await sdk.profile.getUserTrades({ tradingPairId: '123e4567-e89b-12d3-a456-426614174000' });
// Funding payment history (margin positions)
await sdk.profile.listFundingPayments({ pageSize: 20 });
await sdk.profile.listFundingPayments({ tradingPairId: '123e4567-e89b-12d3-a456-426614174000' });
// Sub-accounts (master account)
await sdk.subAccounts.list();
// Testnet faucet (a valid Cloudflare Turnstile token is required on every mint)
await sdk.faucet.mint({ turnstileToken: "" });
```
## Profile Data
Retrieve the authenticated user's core profile metadata.
**Returns:**
* `id`: Unique user identifier
* `address`: Wallet address
* `username`: Display name (nullable)
* `accountType`: Account type (e.g. `"master"`)
* `canWithdraw`: Whether the account can withdraw funds
* `takerFeeBps`: **Deprecated.** Flat taker fee rate in basis points; echoes the pre-tiered pair-level rate, not the per-user tiered fee. Use `fees.getMyFeeTier` for the caller's real rate
* `makerFeeBps`: **Deprecated.** Flat maker fee rate in basis points; echoes the pre-tiered pair-level rate, not the per-user tiered fee. Use `fees.getMyFeeTier` for the caller's real rate
* `applicationTakerFeeBps`: Additional taker fee in basis points contributed by the application, if any (optional)
* `applicationMakerFeeBps`: Additional maker fee in basis points contributed by the application, if any (optional)
* `createdAt`: Account creation timestamp (ISO 8601)
## Token Balances
Fetch paginated token balances for the authenticated user
Cursor-paginated; there is no page-number (offset) mode.
**Parameters:**
* `pageSize` (optional): Number of items per page, 1 to 1000 (default: 20)
* `pageToken` (optional): Pagination cursor:
* **omitted** or **`""` (empty string)** — start a cursor walk from the lowest token address
* **a previous `nextPageToken`** — resume the walk from that cursor
There is no `page`: the SDK rejects one with a `ValidationError` before sending, and a raw REST request carrying it gets `400`. The walk seeks on `(token, assetId)` over the set the server builds after overlaying the matching engine's authoritative amounts, so a balance that appears or is zeroed mid-walk no longer shifts the rows behind it across the resume point.
**Returns:**
* `balances`: Array of AccountBalance objects
* `pageSize`: Items per page
* `total`: Total number of balances — exact, the set is bounded by the asset universe
* `totalPages`: Total number of pages
* `nextPageToken`: Cursor for the next page, `""` once the walk is exhausted. Each page over-fetches one row, so an empty token is definitive — follow a non-empty token, stop on an empty one
Get the user's balance for a specific asset by its UUID
**Parameters:**
* `assetId`: Asset UUID (e.g., '123e4567-e89b-12d3-a456-426614174000')
**Returns:**
* `token`: Token contract address
* `symbol`: Token symbol
* `decimals`: Token decimal places
* `availableBalance`: Available balance for trading
* `lockedBalance`: Balance locked in orders
* `totalBalance`: Total balance (available + locked)
* `version` (optional): Opaque, non-negative spot-row revision. Compare only for the same authenticated user, application, and token; higher is newer. Absent and 0 both mean unranked and never participate in ordering: the row predates this field or came from a producer that does not stamp it, so normalize both to the same thing and fall back to a full field comparison rather than ranking an unranked row as oldest. A server mid-rolling-deploy sends no key at all, and `undefined <= n` is false, so an un-normalized guard silently passes.
## Transaction History
Fetch paginated ledger history — deposits, withdrawals, trades, fees, funding payments, liquidations, interest, rewards, and collateral transfers — with optional server-side filtering
Cursor-paginated; there is no page-number (offset) mode.
**Parameters:**
* `pageSize` (optional): Number of items per page, 1 to 1000 (default: 20)
* `pageToken` (optional): Pagination cursor:
* **omitted** or **`""` (empty string)** — start a cursor walk from the newest row (the oldest under `orderBy: "ASC"`)
* **a previous `nextPageToken`** — resume the walk from that cursor
There is no `page`: the SDK rejects one with a `ValidationError` before sending, and a raw REST request carrying it gets `400`. The filters and `orderBy` must match the request that minted the token.
* `orderBy` (optional): Sort direction for `createdAt` — `"ASC"` or `"DESC"` (default `"DESC"`)
* `entryType` (optional): Filter by entry type — `LedgerEntryType`: `"CREDIT"`, `"DEBIT"`, `"LOCK"`, `"UNLOCK"`, `"FEE"`
* `transactionType` (optional): Filter by transaction type — `TransactionType`: `"DEPOSIT"`, `"WITHDRAWAL"`, `"TRADE"`, `"FEE"`, `"FUNDING"`, `"LIQUIDATION"`, `"INTEREST"`, `"REWARD"`. Filter values and returned values are both upper-case (a funding row reports `transactionType: "FUNDING"`, `entryType: "DEBIT" | "CREDIT"`), and the server matches the filter case-insensitively. The lower-case forms are the live `movements` WebSocket shape, not this one
* `assetId` (optional): Filter by asset UUID
**Returns:**
* `movements`: Array of `LedgerMovement` objects. A `funding` row is a collateral USD delta rather than a token transfer: `amount` is a magnitude with the direction in `entryType`, `decimals` is `0`, and `amountRaw` equals `amount`. It shares its `id` with the live `movements` WebSocket frame, so dedupe by `id` across the two feeds — but the two payloads are not identical: the REST row is upper-case and resolves `assetId`, `symbol` and `decimals` — falling back to the pair's quote asset when the margin account's collateral binding is unset, so these carry a real asset rather than a placeholder, while the WebSocket frame is lower-case and carries no resolved asset id. See [Account Balances](/developers/account-balances#movements)
* `pageSize`: Items per page
* `total`: Matching movements, bounded by the server count ceiling — a lower bound when `totalCapped` is `true`
* `totalPages`: Pages at this `pageSize`, bounded by the same ceiling
* `nextPageToken`: Cursor for the next page, `""` once the walk is exhausted. Each page over-fetches one row, so an empty token is definitive — follow a non-empty token, stop on an empty one. Do not drive the loop off `total`: a user past the ceiling would stop early
* `totalCapped`: `true` when more than the ceiling matched, so `total` / `totalPages` are a lower bound; `false` when exact, including exactly the ceiling
## Portfolio Analytics
Portfolio analytics are documented on the [Portfolio Analytics](/sdk/typescript/portfolio) page. That page covers the authoritative `sdk.portfolio` methods as well as the older `sdk.profile.getPortfolioStats` and `sdk.profile.getPortfolioChart`.
## Trade History
Fetch trade history for the authenticated user, cursor-paginated; there is no page-number (offset) mode.
**Parameters:** all optional
* `pageSize`: Number of items per page (default: 20); up to 1000
* `tradingPairId`: Filter by trading pair UUID
* `pageToken`: Pagination cursor:
* **omitted** or **`""` (empty string)** — start a cursor walk over full trade history (hot and archived rows merged), newest first
* **a previous `nextPageToken`** — resume the walk from that cursor
**Returns:**
* `trades`: Array of `UserTrade` objects
* `pageSize`: Items per page
* `nextPageToken`: Cursor for the next page — non-empty whenever a page returns rows, empty when the walk is exhausted
This listing reports no item count: the `trades` table is too large to count behind a filter. Build "load more" UIs on `nextPageToken` rather than "page N of M".
**`UserTrade` fields:**
* `tradeId`: Trade unique identifier
* `tradingPairId`: Trading pair UUID
* `price`: Execution price (normalized string)
* `quantity`: Executed quantity (normalized string)
* `quoteVolume`: Price × quantity (normalized string)
* `side`: `"BUY"` or `"SELL"` from the user's perspective
* `fee`: Total fee paid for this trade (normalized string): `monacoFee + builderFee`. For a taker, Monaco's taker fee plus the frontend's builder fee; for a maker, the maker fee (negative for a rebate)
* `monacoFee` (optional): Monaco's component of `fee` (normalized string)
* `builderFee` (optional): The frontend's builder fee component of `fee` (normalized string); always `"0"` for a maker, since builder fees are charged to takers only
* `executedAt`: Trade execution timestamp (ISO 8601)
Walk full trade history with cursor pagination, yielding one trade at a time. Accepts `tradingPairId` and `pageSize`; the cursor is managed internally.
```typescript theme={null}
for await (const trade of sdk.profile.iterateUserTrades()) {
// process trade
}
```
## Funding Payments
Fetch the authenticated user's funding payment history for margin positions, with pagination and optional filters.
Cursor-paginated; there is no page-number (offset) mode.
**Parameters:**
* `pageSize` (optional): Number of items per page, 1 to 1000 (default: 20)
* `pageToken` (optional): Pagination cursor:
* **omitted** or **`""` (empty string)** — start a cursor walk from the newest payment
* **a previous `nextPageToken`** — resume the walk from that cursor
There is no `page`: the SDK rejects one with a `ValidationError` before sending, and a raw REST request carrying it gets `400`. The walk seeks on `(createdAt, id)` descending — not `periodEnd`, which the removed mode led its sort with and which the risk engine leaves unset when a payment carries no window. The filters must match the request that minted the token.
* `tradingPairId` (optional): Filter by trading pair UUID
* `positionId` (optional): Filter by margin position UUID
* `marginAccountId` (optional): Filter by margin account UUID
**Returns:**
* `records`: Array of `FundingPayment` objects
* `pageSize`: Items per page
* `total`: Matching payments, bounded by the server count ceiling — a lower bound when `totalCapped` is `true`
* `totalPages`: Pages at this `pageSize`, bounded by the same ceiling
* `nextPageToken`: Cursor for the next page, `""` once the walk is exhausted. Each page over-fetches one row, so an empty token is definitive — follow a non-empty token, stop on an empty one. Do not drive the loop off `total`: a user past the ceiling would stop early
* `totalCapped`: `true` when more than the ceiling matched, so `total` / `totalPages` are a lower bound; `false` when exact, including exactly the ceiling
**`FundingPayment` fields:**
* `id`: Funding payment UUID
* `positionId`: Margin position UUID
* `marginAccountId`: Margin account UUID
* `tradingPairId`: Trading pair UUID
* `fundingRate`: Funding rate applied for the epoch (normalized string)
* `positionSize`: Absolute position size at settlement (normalized string)
* `paymentAmount`: Signed funding amount; positive means paid, negative means received (normalized string)
* `direction`: `"PAID"` or `"RECEIVED"`
* `periodStart`: Funding window start timestamp (ISO 8601, optional)
* `periodEnd`: Funding window end timestamp (ISO 8601, optional)
* `createdAt`: Funding payment creation timestamp (ISO 8601, optional)
## Self-Trade Prevention Default
Read the wallet's self-trade-prevention default — the mode the matching engine applies to an order that does not carry its own `selfTradePreventionMode`.
**Returns:**
* `mode`: `"CANCEL_MAKER" | "CANCEL_TAKER" | "CANCEL_BOTH" | "SKIP"` — the effective mode
* `isCustom`: boolean — `true` when this wallet explicitly set a default, `false` when the platform default `CANCEL_MAKER` is being inherited
The setting is **wallet-scoped**: one default per wallet address, shared by every application and account using it. The read serves an asynchronous projection, so a read issued immediately after `setSelfTradePreventionDefault` can briefly lag that call's own response.
**Throws:**
* REST `400` / gRPC `InvalidArgument` when the authenticated session's wallet is not a canonical EVM address (`0x` + 40 hex) — the setting is keyed by wallet, so a legacy non-EVM session has none to read
* REST `401` / gRPC `Unauthenticated` when authentication is missing or invalid
* REST `403` / gRPC `PermissionDenied` when a delegated-agent session calls it — an agent can neither read nor set this default
* REST `429` / gRPC `ResourceExhausted` once read-budget enforcement is enabled: this is an authenticated read and draws your account's shared read budget, with the retry interval on the body's `details.retryAfter` or a gRPC `RetryInfo` detail. See [Rate limits](/developers/rate-limits)
* REST `500` / gRPC `Internal` for an internal failure
```typescript theme={null}
const stp = await sdk.profile.getSelfTradePreventionDefault();
console.log(`Mode: ${stp.mode} (explicitly set: ${stp.isCustom})`);
```
Set the wallet's self-trade-prevention default. See [Self-Trade Prevention](/developers/trade#self-trade-prevention) for what each mode does.
**Parameters:**
* `mode`: `"CANCEL_MAKER" | "CANCEL_TAKER" | "CANCEL_BOTH" | "SKIP"` — the SDK validates against these exact uppercase tokens and throws before sending, so pass them verbatim. The lowercase forms the server would accept never reach it
**Returns:** the setting now in force, same shape as `getSelfTradePreventionDefault`.
The write is sequenced through the matching engine and returns only after the durable append, so the wallet's **next order already resolves the new mode**. A per-order `selfTradePreventionMode` always overrides this default.
The call sets the mode outright rather than applying a delta, so sending the same `mode` again is safe and a retry after a lost response cannot land a different value than the one you asked for. Concurrent sets for one wallet are last-write-wins in sequencer order, so serialize them per wallet if the final mode matters, and re-read with `getSelfTradePreventionDefault` after a transient failure rather than assuming the write was lost.
**Throws:**
* `ValidationError` (exported by `@0xmonaco/types`) when `mode` is not one of the four tokens — thrown before any network request
* REST `400` / gRPC `InvalidArgument` when a raw caller sends an unknown mode, or when the authenticated session's wallet is not a canonical EVM address (`0x` + 40 hex)
* REST `401` / gRPC `Unauthenticated` when authentication is missing or invalid
* REST `403` / gRPC `PermissionDenied` when a delegated-agent session attempts the change
* REST `429` / gRPC `ResourceExhausted` when the per-account settings-write budget is spent (about one write per second, burst 10, shared with the copy trading lead trader and follow writes). Nothing was applied; retry after `retryAfter`. See [Rate Limits](/developers/rate-limits#settings-and-copy-trading-writes)
* REST `500` / gRPC `Internal` for an internal failure
* REST `503` / gRPC `Unavailable` when the matching engine is unreachable or the sequenced write fails transiently. Do not read it as proof the setting is unchanged — the outcome is **unknown**: `is_transient_me_failure` also classifies `DeadlineExceeded` and `Cancelled` as transient, so the sequenced write may have committed before the response was lost. Re-read with the getter, or simply retry the same `mode` — the call sets an absolute value, so a replay cannot land anything other than what you asked for
```typescript theme={null}
const stp = await sdk.profile.setSelfTradePreventionDefault("SKIP");
console.log(`Mode is now ${stp.mode}`);
```
## Sub-Accounts
Manage a master account's sub-accounts and their legacy per-asset spending limits. All
methods are session-authenticated. The limit mutations (`createLimit`,
`updateLimit`, `deleteLimit`) additionally require the master account to hold the
`ManageSubAccounts` permission — enforced server-side, so a non-master or
unpermissioned caller receives a 403.
List the authenticated master account's sub-accounts with their balances.
**Returns:**
* `subAccounts`: Array of `SubAccount` objects, each with `id`, `address`, `username` (nullable), `canWithdraw`, `createdAt`, and `balances` (array of `AccountBalance`)
* `total`: Total number of sub-accounts
Sub-account spending limits are not enforced:
* `createLimit` and `updateLimit` fail with `400`: `Sub-account spending limits are not enforced and can no longer be configured`.
* `getLimits` and `deleteLimit` act only on limits created before that change.
* All four methods are scheduled for removal.
Create a per-asset spending limit on a sub-account. Requires the `ManageSubAccounts` permission.
**Body:**
* `subAccountId`: Sub-account UUID to create the limit for
* `assetId`: Asset UUID to limit
* `maxAmount`: Maximum amount allowed, in token units (string)
* `dailyLimit` (optional): Maximum daily spending limit, in token units (string)
**Returns:** `{ limit }` — the created `SubAccountLimit`.
Get the limits configured for a sub-account.
**Parameters:**
* `subAccountId`: Sub-account UUID
**Returns:** `{ limits }` — array of `SubAccountLimit` objects.
Partially update a sub-account's per-asset limit. Requires the `ManageSubAccounts` permission. The sub-account and asset are taken from the path; all body fields are optional.
**Parameters:**
* `subAccountId`: Sub-account UUID
* `assetId`: Asset UUID
**Body (all optional):**
* `maxAmount`: New maximum amount, in token units (string)
* `dailyLimit`: New maximum daily spending limit, in token units (string)
* `isActive`: Whether the limit is active
**Returns:** `{ limit }` — the updated `SubAccountLimit`.
Delete a sub-account's per-asset limit. Requires the `ManageSubAccounts` permission. Resolves with no value.
**Parameters:**
* `subAccountId`: Sub-account UUID
* `assetId`: Asset UUID
## Faucet
Mint the full set of testnet tokens to the authenticated user. Testnet-only, session-authenticated, and rate-limited per wallet and application (default 1 request per rolling 24h; the same wallet has its own quota on each application). Performs real on-chain mints and resolves only when every token confirms on-chain; it throws on `401` (not authenticated), `403` (delegated-agent session), `429` (quota exhausted), `502` (mints failed or only partially confirmed — the error carries the minted/failed breakdown), `500` (internal error), or `503` (faucet unavailable, safe to retry). Wrap the call in a `try/catch`.
**Parameters:**
* `turnstileToken`: string (required) - Cloudflare Turnstile captcha token. Requests without a valid token are rejected with `400`.
**Returns (on resolve):**
* `minted`: Array of confirmed minted tokens, each with `assetId`, `symbol`, `amount`, `txHash`. A mint credits spot and margin separately, so a token can appear twice — once as `USDC` and once as `USDC (margin)`
* `failed`: Array of tokens that failed to mint (`assetId`, `symbol`, `error`); empty on a successful resolve — populated on the thrown `502` error instead
* `remainingRequests24h`: Remaining faucet requests in the next 24h
Read the authenticated wallet's faucet quota on the calling application. Throws on `401` (not authenticated), `403` (delegated-agent session), `429` (read budget exhausted), `500` (internal error reading quota history), or `503` (quota configuration unavailable, safe to retry).
**Returns:**
* `remainingRequests24h`: Remaining faucet requests in the rolling 24h window
* `dailyLimit`: The application's daily request limit
* `nextAvailableAt` (optional): ISO 8601 time the next request frees; omitted when the window is empty or the limit is 0
* `recentRequests`: Up to five newest-first request groups from the calling application. Each group holds the mints that share a minute — `requestedAt` is truncated to the minute — with a `minted` array (`assetId`, `symbol`, `amount`, optional `txHash`); it is a display grouping, and the quota counts each request individually
# Market Pulse
Source: https://docs.0xmonaco.com/sdk/typescript/pulse
Cached market news and sentiment feed
The Pulse feed is a backend-cached, cross-market news and sentiment snapshot. It is **public** — no authentication is required — and refreshes hourly server-side.
```typescript theme={null}
import { MonacoSDK } from "@0xmonaco/core";
// Public — works without a wallet or session
await sdk.pulse.getPulse();
```
## Get Pulse
Fetch the current market pulse snapshot. No authentication required.
**Returns:** `GetPulseResponse`:
* `generatedAt`: string — ISO 8601 UTC time the snapshot was produced. The REST response derives its `ETag` header from this value (see [Behavior](#behavior)).
* `watchlist`: string\[] — the asset universe the snapshot was synthesized against.
* `items`: `PulseItem[]` — pre-sorted (watchlist order, `"MARKET"` last, newest first per asset).
Each `PulseItem`:
* `asset`: string — a watchlist ticker (e.g. `"BTC"`) or `"MARKET"` for a cross-asset story.
* `sentiment`: string — `"Bullish"`, `"Neutral"`, or `"Bearish"`.
* `summary`: string — a synthesized summary (max 240 characters).
* `updatedAt`: string — timestamp of the most recent source; render as relative time client-side.
* `sources`: `PulseSource[]` — provenance for the item.
Each `PulseSource`:
* `url`: string — source article or post URL.
* `author`: string — source author or publication name.
* `source`: string — `"twitter"` or `"news"`.
* `createdAt`: string — source publication time (ISO 8601).
* `relevance`: number — relevance score, `1` (low) to `5` (high).
**Example:**
```typescript theme={null}
const pulse = await sdk.pulse.getPulse();
console.log(`Snapshot generated at ${pulse.generatedAt}`);
for (const item of pulse.items) {
console.log(`[${item.sentiment}] ${item.asset}: ${item.summary}`);
for (const src of item.sources) {
console.log(` ${src.source} · ${src.author} (relevance ${src.relevance}) — ${src.url}`);
}
}
```
## Behavior
* **Hourly refresh.** The snapshot is regenerated server-side about once an hour; `generatedAt` tells you when.
* **Conditional requests (raw REST).** The `GET /api/v1/pulse` response carries an `ETag` header — the `generatedAt` value wrapped in quotes, e.g. `"2026-07-22T15:17:14Z"`. To skip an unchanged snapshot, send that header value back **verbatim** (quotes included) as `If-None-Match`, and the server replies `304 Not Modified`. This is a raw-REST/gateway optimization: `sdk.pulse.getPulse()` resolves to the response body only and does not expose or set headers, so conditional requests are not available through the SDK method.
* **Cold cache.** The endpoint returns `503` until the backend cache warms after a deploy — retry shortly.
* **No dedicated rate limit.** This public route has no per-endpoint limiter and does not return `429`. Since the snapshot changes at most hourly, poll no more than about once per refresh interval anyway.
# Trading
Source: https://docs.0xmonaco.com/sdk/typescript/trades
Place and manage orders
```typescript theme={null}
import { MonacoSDK } from "@0xmonaco/core";
// Initialize SDK with wallet client (see Initialization guide)
// Place limit order (spot)
await sdk.trading.placeLimitOrder(
tradingPairId, // UUID from sdk.market.getTradingPairBySymbol('BTC/USDC')
side, // "BUY" | "SELL"
quantity, // e.g., "0.001"
price, // e.g., "50000"
options? // { tradingMode?, useMasterBalance?, expirationDate?, timeInForce?,
// postOnly?, leverage?, reduceOnly?, marginAccountId?,
// clientOrderId?, idempotencyKey? }
);
// Place market order (spot)
await sdk.trading.placeMarketOrder(
tradingPairId, // UUID from sdk.market.getTradingPairBySymbol('BTC/USDC')
side, // "BUY" | "SELL"
quantity, // e.g., "0.001"
options? // { tradingMode?, slippageTolerance?, leverage?, reduceOnly?,
// marginAccountId?, clientOrderId?, idempotencyKey? }
);
// Order management
await sdk.trading.cancelOrder(orderId); // full cancel
await sdk.trading.cancelOrder(orderId, { remainingQuantityTarget }); // partial: reduce in place, keep queue position
await sdk.trading.batchCancel([id1, id2, id3]);
await sdk.trading.batchCancelAll();
await sdk.trading.batchCancelAll(tradingPairId);
await sdk.trading.replaceOrder(orderId, updates);
// TWAP parent orders (sliced execution across a time window)
await sdk.trading.createTwapOrder(tradingPairId, side, quantity, options);
await sdk.trading.getTwapOrder(twapOrderId);
await sdk.trading.listTwapOrders(params?);
await sdk.trading.cancelTwapOrder(twapOrderId);
await sdk.trading.getTwapOrderTca(twapOrderId);
// Conditional orders (perp TP/SL)
await sdk.trading.getConditionalOrder(conditionalOrderId);
await sdk.trading.cancelConditionalOrder(conditionalOrderId);
await sdk.trading.listConditionalOrders(params?);
// Order queries
await sdk.trading.getPaginatedOrders(params?);
await sdk.trading.getOrder(orderId);
// Fee simulation (before placing orders)
await sdk.fees.simulateFees({
tradingPairId: tradingPairId,
side: "BUY",
price: "50000",
quantity: "0.001"
});
```
## Place Orders
Place a limit order with a specific price. Defaults to GTC (Good Till Canceled) time-in-force.
**Parameters:**
* `tradingPairId`: string - Trading pair UUID (get via `sdk.market.getTradingPairBySymbol('BTC/USDC')`)
* `side`: "BUY" | "SELL" - Order side
* `quantity`: string - Order quantity
* `price`: string - Limit price
* `options?`: Optional settings
* `tradingMode?`: `"SPOT" | "MARGIN"` (default: `"SPOT"`)
* `useMasterBalance?`: boolean - Use master account balance (for sub-accounts)
* `expirationDate?`: string - ISO 8601 date for GTC orders
* `timeInForce?`: `"GTC" | "IOC" | "FOK"` - Time in force (default: `"GTC"`)
* **GTC** (Good Till Canceled): Order stays active until filled or canceled
* **IOC** (Immediate or Cancel): Fill immediately, cancel unfilled portion
* **FOK** (Fill or Kill): Fill completely or reject entirely
* `GTD` is declared in the protos but **never implemented** — the server rejects it. For an order that expires at a set time, send `GTC` with `expirationDate`.
* `postOnly?`: boolean — maker-only guarantee. The order is rejected instead of matching if it would cross the book at admission (`BUY` at/above the best ask, `SELL` at/below the best bid) — REST `400` / gRPC `InvalidArgument` carrying the structured error code `POST_ONLY_WOULD_CROSS` (REST error-envelope `code` field; gRPC `google.rpc.ErrorInfo` reason). Match on the code; the `"post-only order would cross"` message substring is a legacy fallback. `LIMIT` with `GTC` only — rejected on `MARKET` orders and when combined with `IOC`/`FOK`. Persisted with the order and echoed back as `postOnly: true` on order reads. See [Post-Only Orders](/developers/trade#post-only-orders)
* `leverage?`: string — decimal string, e.g. `"10"`; required for normal margin orders, omit only for reduce-only
* `reduceOnly?`: boolean — order can only shrink existing exposure
* `marginAccountId?`: string — optional legacy override; omit for normal perp orders
* `positionSide?`: `"LONG" | "SHORT" | "NONE"` — deprecated. `side` is the authoritative direction; if supplied, `positionSide` must agree (`BUY`↔`LONG`, `SELL`↔`SHORT`)
* `clientOrderId?`: string — a correlation handle you choose (≤64 chars from `A-Za-z0-9._:-`, whitespace trimmed, blank treated as absent). Echoed back on order detail/list reads and on every `orders` WebSocket event. Uniqueness is enforced only among your **resting** orders (`409 CLIENT_ORDER_ID_CONFLICT`); an order that never rests — `MARKET`, `IOC`/`FOK`, or a `LIMIT` that fills on arrival — does not take the handle. It is **not** an idempotency key: only a retained explicit `idempotencyKey` makes the same create safe to replay within 24 hours. Without that key, or after it expires, resubmitting after a lost response can execute a second time — reconcile against the order stream (`sdk.ws.userOrders`), not the order list, which is replica-backed and can omit an order that did land. See [Client Order IDs](/developers/trade#client-order-ids)
* `selfTradePreventionMode?`: `"CANCEL_MAKER" | "CANCEL_TAKER" | "CANCEL_BOTH" | "SKIP"` — how the engine resolves this order meeting one of your **own** resting orders (your whole wallet family, across applications and sub-accounts). Overrides the wallet default set via [`sdk.profile.setSelfTradePreventionDefault`](/sdk/typescript/profile); omit it to inherit that, or the platform default `CANCEL_MAKER`. The taker's mode governs the whole step — resting orders' modes are never consulted. Echoed back on order reads and `orders` WebSocket events as the value you **requested**, absent when you sent none. See [Self-Trade Prevention](/developers/trade#self-trade-prevention)
* `idempotencyKey?`: string — durable submission key (1–64 characters from `A-Za-z0-9._:-`). The SDK generates one for this call when omitted. Retain and reuse an explicit key with the same payload to recover across calls or restarts within 24 hours; a new key can execute a new order. See [Safe Retries](/developers/trade#safe-retries-with-idempotency-keys)
Perps are one-way — `side` sets the direction. Attach TP/SL at entry with `takeProfit`/`stopLoss` (margin entry orders only; legs activate when the order fills and cancel if it is cancelled or expires unfilled), or attach to the open position with [`sdk.positions.attachPositionTpSl`](/sdk/typescript/positions). A `trailingStop` leg (`ParentTrailingStopLegParams`: `trailBps`, `activationPrice?`, `quantity?` / `closePosition?`, `slippageToleranceBps?`, `expiresAt?`) attaches the same way but arms only when the order **fully** fills. `trailingStopOrderId` is present only when the trailing stop materializes: the order fully fills on arrival or leaves a resting remainder. A partial fill whose remainder is cancelled (an `IOC` or `MARKET` order) creates no trailing stop and omits the id. See [Trailing Stop](/developers/trade#trailing-stop).
**Perp example:**
```typescript theme={null}
// Open a 10x long with TP/SL attached at entry
await sdk.trading.placeLimitOrder(tradingPairId, "BUY", "1.5", "2000", {
tradingMode: "MARGIN",
leverage: "10",
takeProfit: { triggerPrice: "2200", orderType: "MARKET" },
stopLoss: { triggerPrice: "1900", orderType: "LIMIT", limitPrice: "1895", timeInForce: "IOC" },
});
// Or attach / adjust TP/SL once the position is open
await sdk.positions.attachPositionTpSl(positionId, {
takeProfit: { triggerPrice: "2200", orderType: "MARKET" },
stopLoss: { triggerPrice: "1900", orderType: "LIMIT", limitPrice: "1895", timeInForce: "IOC" },
});
```
Place a market order. Fills execute inside a mandatory server-side price band, 1,000 bps (10%) by default (see [Market-order price protection](/trading-mechanics/spot#market-order-price-protection); a perp market can set its own, see [Perp price band](/trading-mechanics/perps#perp-price-band)): a market order can partially fill with the remainder cancelled, or be rejected when no liquidity sits inside the band. Triggered TP/SL market legs use a wider 1,200 bps (12%) band by default.
**Parameters:**
* `tradingPairId`: string - Trading pair UUID (get via `sdk.market.getTradingPairBySymbol('BTC/USDC')`)
* `side`: "BUY" | "SELL" - Order side
* `quantity`: string - Order quantity
* `options?`: Optional settings
* `tradingMode?`: `"SPOT" | "MARGIN"` (default: `"SPOT"`)
* `slippageTolerance?`: number - tightens the market's price band (e.g. 0.01 for 1%); cannot widen it
* `leverage?`: string — decimal string; required for normal margin orders, omit only for reduce-only
* `reduceOnly?`: boolean — order can only shrink existing exposure
* `marginAccountId?`: string — optional legacy override; omit for normal perp orders
* `positionSide?`: `"LONG" | "SHORT" | "NONE"` — deprecated; `side` is authoritative
* `clientOrderId?`: string — correlation handle echoed on order reads and `orders` WebSocket events (≤64 chars from `A-Za-z0-9._:-`); not an idempotency key. See [Client Order IDs](/developers/trade#client-order-ids)
* `selfTradePreventionMode?`: `"CANCEL_MAKER" | "CANCEL_TAKER" | "CANCEL_BOTH" | "SKIP"` — how the engine resolves this order meeting one of your **own** resting orders (your whole wallet family, across applications and sub-accounts). Overrides the wallet default set via [`sdk.profile.setSelfTradePreventionDefault`](/sdk/typescript/profile); omit it to inherit that, or the platform default `CANCEL_MAKER`. The taker's mode governs the whole step — resting orders' modes are never consulted. Echoed back on order reads and `orders` WebSocket events as the value you **requested**, absent when you sent none. See [Self-Trade Prevention](/developers/trade#self-trade-prevention)
* `idempotencyKey?`: string — durable submission key (1–64 characters from `A-Za-z0-9._:-`). The SDK generates one for this call when omitted. Retain and reuse an explicit key with the same payload to recover across calls or restarts within 24 hours. See [Safe Retries](/developers/trade#safe-retries-with-idempotency-keys)
**Perp example:**
```typescript theme={null}
// Reduce-only market close of a short
await sdk.trading.placeMarketOrder(tradingPairId, "BUY", "0.5", {
tradingMode: "MARGIN",
reduceOnly: true,
});
```
Create 1–100 orders. Every item is processed independently and keeps its input position in `results`; one failure does not roll back successful siblings.
`BatchCreateOrderParams` includes `tradingPairId`, `orderType`, `side`, and `quantity`, plus the single-create margin and execution options except attached `takeProfit` / `stopLoss` / `trailingStop`, which batch items do not support. `price` is required for `LIMIT`; `timeInForce` is valid only for `LIMIT`; `postOnly` requires `LIMIT` + `GTC`. Each item also accepts `clientOrderId?`, `idempotencyKey?` and `selfTradePreventionMode?`. `selfTradePreventionMode` **is** covered by `BatchCreateOrderItemSchema`, unlike `clientOrderId`. `BatchCreateOrdersSchema` validates the whole array before anything is sent, so one item's unknown mode throws `ValidationError` and **no item executes** — the per-item `results[].error.code === "INVALID_SELF_TRADE_PREVENTION_MODE"` is a raw REST/gRPC caller's shape, never an SDK caller's. A `FOK` item under `CANCEL_BOTH` that meets one of your own orders does reach the engine and fails as that item with `SELF_TRADE_NOT_ALLOWED`; neither rolls back its siblings. The SDK generates a separate key for each item that omits one. Retain explicit per-item keys when an ambiguous result must be retried across calls; a key protects only its own item and does not make the batch atomic.
**Returns:** `{ totalRequested, totalSucceeded, totalFailed, results }`. Each result contains `orderId`, optional `matchResult`, and optional `error: { code, message }`; absence of `error` means success.
The TypeScript SDK validates the batch envelope and every field covered by `BatchCreateOrderItemSchema` before sending; it throws `ValidationError` (exported by `@0xmonaco/types`, not `@0xmonaco/core`) for those client-schema failures or more than 100 items. `clientOrderId` is typed but is not covered by that client schema, so an invalid handle reaches the server and returns as the item's `results[].error.code === "INVALID_CLIENT_ORDER_ID"`. Raw REST/gRPC callers receive REST `400` / gRPC `InvalidArgument` for whole-request errors; REST `401` / gRPC `Unauthenticated` when authentication is missing; and REST `500` / gRPC `Internal` for an internal failure. A raw batch with no item key can also return whole-request REST `503` / gRPC `Unavailable` after a deadline or transport failure; its outcome is unknown, and without retained keys it cannot be safely retried. Other item-level validation and placement failures remain in `results`. Because the TypeScript SDK keys every item, an internal deadline expiry instead returns overall REST `200` / gRPC `OK` with `results[].error.code === "SERVICE_UNAVAILABLE"` on that item. Inspect every result, reconcile its state, and reuse the same explicit item key before retrying.
See [Batch Create](/developers/trade#batch-create) and [Safe Retries](/developers/trade#safe-retries-with-idempotency-keys).
## Order Management
Cancel an order. Called with just an `orderId`, it fully cancels the order.
Pass `options.remainingQuantityTarget` for a **partial cancel**: the resting order is reduced *in place* to that remaining quantity, keeping its queue position — unlike `replaceOrder`, which retires the order and rests a new one at the back of the book even for a pure size-down. The released delta unlocks exactly as a cancel does (spot unlock / margin reserve).
The target is "leave this much resting", not an amount to remove, so a retried request converges instead of compounding. It must be a positive multiple of the market's quantity step and strictly below the order's current remaining quantity — emptying an order is a full cancel's job, and growing one is a `replaceOrder`. The reduction emits a non-terminal `OrderPartiallyCancelled` event on the `orders` WebSocket channel (the order is still resting); see [WebSockets](/sdk/typescript/websockets#orders-channel).
```typescript theme={null}
// Full cancel
await sdk.trading.cancelOrder(orderId);
// Partial: leave 0.5 resting, keeping queue position
await sdk.trading.cancelOrder(orderId, { remainingQuantityTarget: "0.5" });
```
Cancel multiple specific orders by their IDs.
**Parameters:**
* `orderIds`: string\[] - Array of order IDs to cancel
**Returns:**
* `totalRequested`: number - Total number of orders requested to cancel
* `totalCancelled`: number - Count of successfully cancelled orders
* `totalFailed`: number - Count of failed cancellations
* `results`: `BatchCancelResult[]` — each with `orderId`, optional `cancelledAt`, optional `error: { code, message }`. `cancelledAt` is the matching engine's cancel stamp at millisecond precision — it matches, to the millisecond, the `cancelledAt` a later read of the order returns
**Example:**
```typescript theme={null}
const result = await sdk.trading.batchCancel([
"order-id-1",
"order-id-2",
"order-id-3",
]);
console.log(`Cancelled: ${result.totalCancelled}/${result.totalRequested}`);
// Check individual results — `error` is set only when cancellation failed
result.results.forEach((r) => {
if (r.error) {
console.log(`Order ${r.orderId} failed: ${r.error.message}`);
} else {
console.log(`Order ${r.orderId} cancelled at ${r.cancelledAt}`);
}
});
```
**Note:** Some cancellations may fail while others succeed; failures don't roll back successful cancellations. Use `totalFailed === 0` for an overall-success check.
A batch is capped at **100 order IDs** per request; a longer array is rejected with API `400` (`At most 100 orders per batch request`). `batchCancelAll` enumerates active orders server-side, with a separate cap of **20,000 matching active orders**.
Cancel all active orders, optionally filtered by trading pair.
The request is capped at **20,000 matching active orders**. Above that cap, it cancels nothing and returns REST `400`; narrow the request by trading pair or cancel explicit batches of at most 100 IDs. This all-or-nothing cap check happens before any cancellation.
**Parameters:**
* `tradingPairId` (optional): string - Trading pair UUID to filter cancellation. If omitted, cancels all active orders globally.
**Returns:** Same shape as `batchCancel` (`totalRequested`, `totalCancelled`, `totalFailed`, `results: BatchCancelResult[]`)
**Throws:**
* REST `400` / gRPC `InvalidArgument` when the pair UUID is invalid or more than 20,000 matching active orders would be cancelled
* REST `401` / gRPC `Unauthenticated` when authentication is missing or invalid
* REST `403` / gRPC `PermissionDenied` when a delegated session omits the required pair scope or lacks permission
* REST `429` / gRPC `ResourceExhausted` when the risk-reduction budget is spent — mass exit costs **one** item per call, however many orders it cancels, and the call must fit both your account's budget and your [account family's](/developers/rate-limits#account-families) shared one. Honor the interval the transport carries: REST puts it in the body's `details.retryAfter`, gRPC attaches a `google.rpc.RetryInfo` detail to the status
* REST `500` / gRPC `Internal` for an internal matching-engine failure
* REST `503` / gRPC `Unavailable` for a transient matching-engine or transport failure
**Example:**
```typescript theme={null}
// Cancel ALL active orders globally
const allResult = await sdk.trading.batchCancelAll();
console.log(`Emergency exit: cancelled ${allResult.totalCancelled} orders`);
// Cancel all orders for a specific trading pair
const pair = await sdk.market.getTradingPairBySymbol('ETH/USDC');
const pairResult = await sdk.trading.batchCancelAll(pair.id);
console.log(`Cancelled ${pairResult.totalCancelled} ETH/USDC orders`);
```
Modify an existing order's price or quantity. `quantity` is the order's new **total**, not a fresh size: on a partially filled order the replacement rests `total - filled` and locks collateral for that remainder only (FIX cancel/replace convention: `LeavesQty = OrderQty - CumQty`), so the total must exceed the filled amount — replacing a 96/100-filled order with `quantity: "200"` rests 104 and reads as 96/200 filled. Omitting `quantity` keeps the order's total. The replacement inherits the original's fill history — filled quantity, VWAP, and realized fee/payment aggregates carry onto the new order — so summing those aggregates across a replacement chain double-counts (per-order and trade-level reads are unaffected). Margin reduce-only orders follow the same rule; what the replacement rests must not exceed the live position. Pass `postOnly` to re-state maker-only intent for the replacement — it is never inherited from the original order. A single `replaceOrder` validates `postOnly` (and every other check) *before* cancelling the original, so a post-only crossing rejection leaves the original resting, untouched. See [Post-Only Orders](/developers/trade#post-only-orders). A replacement is a new order with a new id, so `clientOrderId` is also not inherited — restate it to keep the handle. `selfTradePreventionMode` behaves the same way: the replacement carries its own value and inherits nothing from the original, falling back to your wallet default when omitted. See [Self-Trade Prevention](/developers/trade#self-trade-prevention).
**Two kinds of order cannot be replaced at all.** A TWAP slice child is refused — cancel the parent instead — and so is the `LIMIT` leg a take-profit or stop-loss fired into, any order carrying `conditionalOrderId`: a replacement would rest under a new id the conditional order does not point at, severing the link in both directions. Both are `INVALID_ORDER` rejections, the conditional one naming the conditional order, and in both cases the original stays resting. Cancelling a triggered leg is unaffected.
Replace 1–100 resting orders in one request. **Cancel-first**: the matching engine cancels every item's original before placing any replacement, in request order, so a two-sided quote can shift across its own resting prices in one call — see [Batch Replace](/developers/trade#batch-replace) for the failure semantics that follow from that ordering.
`BatchReplaceOrderParams` carries `orderId` plus the optional `price`, `quantity`, `useMasterBalance`, `postOnly`, `clientOrderId` and `selfTradePreventionMode`. Although each is optional at the type level, `BatchReplaceOrderItemSchema` requires **at least one of `price` or `quantity`** per item — an item that only restates `selfTradePreventionMode` or `postOnly` throws `ValidationError` before sending. `quantity` is the order's new **total**, exactly as in `replaceOrder`. A replacement is a new order with a new id, so `postOnly`, `clientOrderId` and `selfTradePreventionMode` are **never inherited** — restate each one you want to keep. There is no `timeInForce` input: the replacement **inherits** the original's, and since only an already-resting order is eligible that is always `GTC`/`GTD` — so a replacement is never `IOC`/`FOK` and the `FOK` + `CANCEL_BOTH` self-trade rejection cannot arise here. A successful item is not necessarily resting either: `CANCEL_TAKER`, `CANCEL_BOTH`, or a `SKIP` remainder crossing one of your surviving orders can cancel the replacement immediately, so read the order's status rather than inferring it from the result.
**Returns:** `{ totalRequested, totalSucceeded, totalFailed, results }`. Each `BatchReplaceResult` carries the required `originalOrderId`, plus optional `newOrderId`, `updatedFields`, `matchResult` and `error: { code, message }` — note it is `originalOrderId`, not `orderId`, so a result is keyed by the order you replaced rather than the one you got back.
As with `batchCreate`, the SDK validates the whole array against `BatchReplaceOrdersSchema` before sending, so an unknown `selfTradePreventionMode` throws `ValidationError` and no item executes; the per-item `INVALID_SELF_TRADE_PREVENTION_MODE` is a raw REST/gRPC caller's shape.
The same two exclusions as `replaceOrder` apply per item: a TWAP slice child and a `LIMIT` leg carrying `conditionalOrderId` are both refused with `INVALID_ORDER`, and the refused item's original stays resting.
## Conditional Orders (Perp TP/SL)
Conditional orders sit dormant until mark price crosses a trigger, then submit the underlying order. Create TP/SL and trailing stops by attaching legs at entry via `takeProfit`/`stopLoss`/`trailingStop` on `placeLimitOrder` / `placeMarketOrder`, or to an open position via [`sdk.positions.attachPositionTpSl`](/sdk/typescript/positions). There is no standalone create call; existing conditionals can be fetched by ID, listed, and cancelled.
Fetch a single conditional (TP/SL) order by its UUID. The `takeProfitOrderId`, `stopLossOrderId` and `trailingStopOrderId` fields returned by `attachPositionTpSl` (and on an entry order's `CreateOrderResponse`) are conditional order UUIDs queryable via this method. A trailing stop reads `conditionType: "TRAILING_STOP"` and additionally carries `trailBps`, `activationPrice?`, `watermarkPrice?` (best mark since arming, from which `triggerPrice` is derived; absent on a stop that armed immediately until the first mark after arming; an `activationPrice` stop is seeded at that level the moment it arms) and `trailArmedAt?` (absent while waiting for `activationPrice`); `state` stays `ACTIVE` both while waiting for activation and while tracking (an entry-attached one is `PENDING_PARENT` until its order fully fills).
**Returns:** `ConditionalOrder` — the same type produced by `listConditionalOrders`
Once `state` is `TRIGGERED`, the response carries `triggeredOrder?: TriggeredOrderSummary` — what the order this conditional fired into has done so far, read from that order's own row at request time: `orderId`, `status`, `filledQuantity`, and optional `averageFillPrice`, `totalTakerFees`, `filledAt` and `terminalReason`. It is absent while the conditional waits on its trigger, absent on a `FAILED` conditional, and absent on the `conditional_orders` WebSocket frames — snapshot and live alike. A `FAILED` conditional has no summary because it never recorded a `triggeredOrderId` to join on, and it may have no close row either: a close the engine **placed** and the book then rejected persists as an order row carrying `conditionalOrderId`, but one refused before placement — a validation failure such as a bad quantity step — produces no order at all. Read `failureReason` on the conditional rather than looking for a close.
`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.
Compare `triggeredOrder.averageFillPrice` against `triggerPrice` rather than assuming they match. The trigger fires when the **mark** crosses the level and the leg then trades against the **book**, and the mark is a reference price rather than the book's midpoint, sampled on a worker tick — so the two prices are independent. A market close usually fills worse than the trigger, by the spread it crosses plus the depth its size consumes, but there is no guaranteed 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. See [Trigger Price Is Not Fill Price](/developers/trade#trigger-price-is-not-fill-price).
**Example:**
```typescript theme={null}
// Open a position, then attach a take-profit leg
await sdk.trading.placeLimitOrder(pair.id, "BUY", "1.5", "2000", {
tradingMode: "MARGIN",
leverage: "10",
});
const { takeProfitOrderId } = await sdk.positions.attachPositionTpSl(positionId, {
takeProfit: { triggerPrice: "2200", orderType: "MARKET" },
});
if (!takeProfitOrderId) throw new Error("Take-profit order was not created");
const tp = await sdk.trading.getConditionalOrder(takeProfitOrderId);
console.log(tp.state, tp.triggerPrice);
```
Cancel an active conditional order.
**Returns:** `{ conditionalOrderId, status, message }`
List conditional orders for the authenticated user. Cursor (`pageToken`) pagination is the only mode.
**Parameters (all optional):**
* `marginAccountId?`: string
* `tradingPairId?`: string
* `state?`: `ConditionalOrderState`
* `pageToken?`: string — pagination cursor:
* **omitted** or **`""`** — start a cursor walk from the newest conditional order
* **a previous `nextPageToken`** — resume the walk from that cursor
* `pageSize?`: number — up to `1000`
**Returns `ListConditionalOrdersResponse`:**
* `orders`: `ConditionalOrder[]`
* `total`: number — matching conditional orders, counted up to a ceiling of 10,000 and saturating there, so a caller with more than that reads the cap rather than a lifetime total
* `nextPageToken`: string — cursor for the next page; minted only while a further page exists, empty on the final page. Follow it until it comes back empty; build "load more" UIs on it rather than "page N of M".
* `pageSize`: number
**Errors:** `400` (invalid query parameters, a malformed `pageToken`, or a `pageToken` replayed against different filters than the ones that minted it), `401` (auth required), `429` (read rate limit — retry after `retryAfter`), `500`.
Rows carry `triggeredOrder` on the same terms as `getConditionalOrder` above: present once `state` is `TRIGGERED` and the close can be read, absent otherwise.
Lifecycle updates stream over the [`conditional_order_update` websocket event](/sdk/typescript/websockets). Those frames never carry `triggeredOrder` — read the close's outcome from REST.
## TWAP Orders
A TWAP (time-weighted average price) order executes a large order as a schedule of market child orders spread across a time window, averaging price over time instead of paying market impact in a single print. You submit one **parent order** and the matching engine plans, places, and tracks the child slices. See [TWAP Orders](/trading-mechanics/twap-orders) for the execution model (slicing, randomization, catch-up, hard stop). All five methods are authenticated and reject delegated-agent sessions.
Create a TWAP parent order. `quantity` must be at least twice the pair's minimum order size so it can be sliced.
**`options` (`CreateTwapOrderOptions`):**
* **Window (required, exactly one):** `durationSeconds` (number, or an int64 string the SDK normalizes to a JSON number; server default bounds 60 s – 30 days) **or** `endTime` (RFC 3339 UTC with a `Z` suffix — a non-UTC offset is rejected, not converted). `startTime` (RFC 3339 UTC) is allowed only alongside `endTime`. Client-side Zod validation enforces the exactly-one-of rule before the request is sent.
* `limitPrice?`: string — parent-level cap; BUY children never execute above it, SELL never below.
* `slippageToleranceBps?`: number — per-slice protective band (default `100`, max `500`).
* `randomize?`: boolean — seeded ±30% size / ±30% timing jitter (default `true`).
* `executionStyle?`: `"TAKER" | "PASSIVE"` (default `"TAKER"`). `TAKER` fires a banded `MARKET` child at every slot. `PASSIVE` rests a post-only `LIMIT` `GTC` child at the same-side touch, chases the touch with throttled reprices, and sweeps only the remainder as a banded `MARKET` child at the slot deadline — maker fills pay the pair's maker fee with no application fee. Passive parents plan on a 60 s target slice interval instead of \~5 s, under the same 2,880-slice cap. See [Passive Execution](/trading-mechanics/twap-orders#passive-execution).
* `reduceOnly?`: boolean — margin only; children inherit and clamp to the live position.
* `tradingMode?`: `"SPOT" | "MARGIN"` (default `"SPOT"`). Margin parents also take `marginAccountId?`, `riskBucketId?`, `marginMode?`, and `leverage?` (required for normal margin TWAPs).
* `useMasterBalance?`, `strategyKey?`: optional.
* `clientTwapId?`: string — idempotency key, ≤36 chars, unique per user + application; a duplicate is rejected with `409`.
* **Conditional activation (optional):** `triggerPrice?` (string) and `triggerDirection?` (`"ABOVE" | "BELOW"`) are both-or-neither and make the parent wait, `PENDING`, until the reference price reaches the level. A conditional parent takes `durationSeconds` only — `startTime`/`endTime` are rejected — because its window floats from the trigger instant. `conditionalExpiry?` (RFC 3339 UTC, `Z` suffix only) bounds the wait, is valid only alongside a trigger, must be in the future, and both defaults to and is capped at 30 days from creation. Client-side Zod enforces the both-or-neither, duration-only, expiry-requires-trigger, and expiry-range rules before the request is sent; the `Z`-suffix rule is enforced server-side. See [Conditional TWAP](/trading-mechanics/twap-orders#conditional-twap-price-triggered-activation).
**Returns `CreateTwapOrderResponse`:** `twapOrderId`, `status`, `message`, `state` (always `PENDING` at create), resolved `marginAccountId` / `riskBucketId`, `plannedSlices`, `firstSliceAt`, `endTime`.
**Errors:** `400` (invalid window bounds, quantity too small or too large to slice, margin field mismatch, per-user active-TWAP cap reached, a trigger already satisfied at creation, a conditional create on a margin market that has no oracle mark yet, a `reduceOnly` margin parent whose risk bucket does not exist or is closed — a missing bucket keeps the usual remedy, while a bucket the request named explicitly and that is closed answers `Risk bucket is closed; this TWAP order cannot reopen it — transfer collateral to the risk bucket first`. A non-reduce-only margin parent is not refused in either case: it provisions the missing bucket, or funds the named closed one, at creation — or a validation refusal from that first-use provisioning, returned as `First-use risk bucket provisioning failed: {detail}`), `401` (auth required or delegated-agent session), `409` (duplicate `clientTwapId`), `429` (a parent costs one order-creation item against your account budget and your [account family's](/developers/rate-limits#account-families) shared one; the retry interval arrives as `details.retryAfter` over REST and as a `google.rpc.RetryInfo` detail over gRPC), `500` (internal server or matching-engine failure, including an internal fault inside first-use bucket provisioning, sanitized to `First-use risk bucket provisioning failed` — that exact text is the gRPC status message, while REST wraps it as `Internal server error: First-use risk bucket provisioning failed`), and `503` / gRPC `Unavailable` (the matching engine is unreachable, or the create's reply was lost in transport).
Treat that `503` as an **unknown** outcome rather than a failure: the parent may already have been accepted. Reconcile before retrying — list your TWAP parents, or resubmit with the same `clientTwapId`, which answers `409` if the original landed.
**Example:**
```typescript theme={null}
const pair = await sdk.market.getTradingPairBySymbol("BTC/USDC");
// Spot TWAP: 0.5 BTC over 10 minutes
const twap = await sdk.trading.createTwapOrder(pair.id, "BUY", "0.5", {
durationSeconds: 600,
limitPrice: "50000",
});
console.log(twap.twapOrderId, twap.state, twap.plannedSlices);
```
Fetch the owner's TWAP parent read model with live progress: `executedQuantity`, `executedNotional`, `averageFillPrice`, `progress` (0–1), `plannedSlices` / `slicesPlaced` / `slicesSkipped`, `nextSliceAt`, `state` (`PENDING` / `ACTIVE` / `COMPLETED` / `CANCELLED`), and `terminalReason` (including end-of-window shortfall, and `trigger expired` for a conditional parent whose trigger never fired). Monitor the slice counters, not the state, for degraded execution. The read model also echoes `executionStyle` (`"TAKER" | "PASSIVE"`) and, for a conditional parent, `triggerPrice`, `triggerDirection`, `conditionalExpiry`, and `triggeredAt` — read the armed window off the parent's `startTime` / `endTime` rather than the create response, whose values are provisional until the trigger fires. (`startAt` / `endAt` are the `twap_orders` WebSocket payload's names for the same pair; the REST read model uses `startTime` / `endTime`.)
List the caller's TWAP parents. Cursor (`pageToken`) pagination is the only mode.
**Parameters (all optional):**
* `state?`: `TwapOrderState`
* `tradingPairId?`: string
* `pageToken?`: string — pagination cursor:
* **omitted** or **`""`** — start a cursor walk from the newest TWAP parent
* **a previous `nextPageToken`** — resume the walk from that cursor
* `pageSize?`: number — up to `1000`
**Returns `ListTwapOrdersResponse`:**
* `orders`: `TwapOrder[]`
* `total`: number — matching TWAP orders, counted up to a ceiling of 10,000 and saturating there, so a caller with more than that reads the cap rather than a lifetime total
* `nextPageToken`: string — cursor for the next page; minted only while a further page exists, empty on the final page. Follow it until it comes back empty; build "load more" UIs on it rather than "page N of M".
* `pageSize`: number
**Errors:** `400` (invalid query parameters, a malformed `pageToken`, or a `pageToken` replayed against different filters than the ones that minted it), `401` (auth required), `429` (read rate limit — retry after `retryAfter`), `500`.
Cancel a TWAP parent. Atomic and immediate — child orders are IOC and never rest, so there is nothing to unwind; quantity already executed stands.
**Returns:** `{ twapOrderId, status, message }`
Fetch the post-execution TCA (transaction cost analysis) report for a **terminal** TWAP parent — available once the parent is `COMPLETED` or `CANCELLED`. The report scores `realizedAvgPrice` (`executedNotional / executedQuantity`, **gross of fees**) against two references over the active window (`windowStartAt` to `windowTerminalAt`, the actual terminal time — never the planned end): the arrival price at the window start and the window-TWAP benchmark, both from the single series named by `benchmarkSource` (`risk_mark_1m` for marked pairs, `ohlcv_1m_close` fallback). `slippageVsArrivalBps` and `performanceVsBenchmarkBps` are signed bps at 4 decimal places — **positive always means the execution beat the reference** (BUY filled below it, SELL above it).
**Returns `GetTwapOrderTcaResponse`:** always-present structural fields (`twapOrderId`, `tradingPairId`, `state`, `side`, `totalQuantity`, `executedQuantity`, `shortfallQuantity`, window bounds), plus six optional analytics fields that are **omitted — key absent, not `null` — when that field cannot be computed honestly**, each on its own condition: `realizedAvgPrice` comes straight from the fills and is absent only when nothing executed; `arrivalPrice`, `benchmarkTwapPrice`, and `benchmarkSource` are absent when no reference series qualified or the window sealed no minute bucket; `slippageVsArrivalBps` / `performanceVsBenchmarkBps` are absent when either of their inputs is. A parent that filled but had no qualifying reference series still reports `realizedAvgPrice`, just no scores — handle the absent-key case on every analytics field.
**Errors:** `400` (invalid TWAP order ID format, or the parent has not reached a terminal state yet), `401` (auth required or delegated-agent session), `404` (unknown ID, or another user's parent — byte-identical responses), `500` (internal server error).
Live progress streams on the authenticated `twap_orders` channel via [`sdk.ws.twapOrders`](/sdk/typescript/websockets#twap-orders-channel).
## Order Queries
Order reads and order WebSocket events expose `version?: number`, an opaque revision for the same order only. Merge a REST or subscription snapshot when `snapshot.version >= local.version`; the snapshot wins ties because several live events can share one sequencer step. Gaps are normal, versions from different orders are not comparable, and an absent version means unknown rather than zero. See the [market-maker reconciliation recipe](/developers/market-maker-runbook#4-reconnect-and-resync).
Conditional-order reads, live conditional updates and subscription snapshots also expose `version?: number`. When both sides carry one, merge them for the same conditional order with `incoming.version >= local.version`; a versioned row supersedes an unversioned one, and two unversioned rows compare on `updatedAt`.
Query orders with filters, cursor-paginated; there is no page-number (offset) mode.
Returns the caller's **book orders** only — `LIMIT` and `MARKET` orders, resting and historical — newest first across full history. Filter `status` to `["SUBMITTED", "PARTIALLY_FILLED"]` for the resting working set. TWAP parents and conditional (TP/SL) orders are not rows here: list them with `listTwapOrders` and `listConditionalOrders`. The close a take-profit or stop-loss fired into is a book order and is listed, as its own `MARKET` or `LIMIT` row carrying `conditionalOrderId` back to the conditional; what that close did is also summarized on the conditional order's `triggeredOrder`.
**Parameters:** all optional
* `status?`: `OrderStatusFilter` — a single `OrderStatus` or an array (e.g. `["SUBMITTED", "PARTIALLY_FILLED"]`), sent as one request
* `tradingPairId?`: string
* `tradingMode?`: `"SPOT" | "MARGIN"`
* `marginAccountId?`: string
* `pageSize?`: number — items per page; up to 1000
* `pageToken?`: string — pagination cursor:
* **omitted** or **`""` (empty string)** — start a cursor walk over full history (hot and archived rows merged), newest first
* **a previous `nextPageToken`** — resume the walk from that cursor
**Returns:** `GetPaginatedOrdersResponse`:
* `orders`: `Order[]` — historical orders
* `nextPageToken`: string — the cursor for the next page; non-empty whenever a page returns rows, empty when the walk is exhausted (a zero-row page)
* `pageSize`: number — items per page
* `total`, `totalPages`: number — the item count and page count matching the filter, exact up to the server ceiling (10,000) and saturating there. A lower bound when `totalCapped` is `true`.
* `totalCapped`: boolean — `true` when **more than** the ceiling matched, so the count stopped there and `total`/`totalPages` are a lower bound ("10,000+"); `false` when `total` is exact (including exactly the ceiling).
**Example (the resting working set):**
```typescript theme={null}
const open = await sdk.trading.getPaginatedOrders({ status: ["SUBMITTED", "PARTIALLY_FILLED"] });
for (const order of open.orders) {
console.log("resting", order.quantity, "@", order.price);
}
// Parent orders live on their own listings:
const parents = await sdk.trading.listTwapOrders({ state: "ACTIVE" });
const triggers = await sdk.trading.listConditionalOrders({ state: "ACTIVE" });
```
**Example (walk full order history):**
```typescript theme={null}
let pageToken = ""; // empty string starts the cursor walk
do {
const res = await sdk.trading.getPaginatedOrders({ pageToken });
for (const order of res.orders) {
// process order
}
pageToken = res.nextPageToken;
} while (pageToken !== "");
```
Walk full order history with cursor pagination, yielding one order at a time. Accepts the `getPaginatedOrders` filters plus `pageSize`; the cursor is managed internally.
```typescript theme={null}
for await (const order of sdk.trading.iterateOrders({ status: "FILLED" })) {
// process order
}
```
Get specific order details.
An order the engine **placed** for a take-profit or stop-loss trigger — including one the book then rejected — carries `conditionalOrderId`, the conditional that fired it, so a close in order history can be attributed without scanning the conditional list. 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. `getPaginatedOrders` carries it on the same terms; **no order WebSocket event or snapshot row does**, so a close first seen over the socket acquires the field on the next REST read.
## Fee Simulation
Simulate fees before placing an order to understand exact costs.
Calculate exact fees for an order before placing it. Requires authentication.
**Parameters:**
* `tradingPairId`: string - Trading pair UUID
* `side`: "BUY" | "SELL" - Order side
* `price`: string - Price per unit
* `quantity`: string - Quantity to trade
* `orderType?`: `"LIMIT" | "MARKET"` - Order type (default: `"LIMIT"`). `MARKET` orders include a slippage buffer in the lock amount.
* `slippageToleranceBps?`: number - Slippage tolerance in basis points (0–1000); only *tightens* the protective band, never widens it. **Only valid when `orderType` is `"MARKET"`** (rejected otherwise). Default: `1000` (10%), matching the market-order price band.
**Returns:**
* `notional`: string - Total order value (price × quantity)
* `monacoTakerFee`: string - Monaco protocol taker fee
* `monacoTakerFeeBpsExact`: string | null - Tier-resolved Monaco taker rate as a decimal bps string (e.g. `"6.5"`) — the exact rate the ledger charges this caller, equal to `monacoTakerFee / notional`. Prefer this over the integer field below
* `monacoMakerRebateBpsExact`: string | null - Tier-resolved Monaco maker rebate as a decimal bps string (negative = rebate, e.g. `"-1"`) — the exact rate the ledger credits this caller
* `monacoTakerFeeBps`: number - **Deprecated.** Whole-bps taker rate; an integer cannot carry fractional tiered rates (e.g. 6.5), so this echoes the flat trading-pair column, not the tier-resolved rate. Use `monacoTakerFeeBpsExact` or `fees.getMyFeeTier`
* `monacoMakerRebate`: string - Monaco maker rebate (negative = rebate)
* `monacoMakerRebateBps`: number - **Deprecated.** Whole-bps maker rebate; same integer limitation as above. Use `monacoMakerRebateBpsExact` or `fees.getMyFeeTier`
* `applicationTakerFee`: string - Application taker fee
* `applicationTakerFeeBps`: number - Application fee in basis points
* `totalTakerFees`: string - Total fees for taker (monaco + application)
* `takerTotalPayment`: string - Total amount taker must pay
* `makerTotalReceipt`: string - Total amount maker receives
* `buyOrderLockAmount`: string | null - Amount locked for BUY orders
* `maxQuantity`: string | null - Maximum quantity affordable at the given price, accounting for fees (null if not authenticated or balance unavailable)
* `maxQuantityRaw`: string | null - `maxQuantity` in RAW (smallest-unit) format
* `slippageToleranceBps`: number | null - Slippage tolerance used in the calculation, echoed back for `MARKET` orders (null for `LIMIT` orders)
**Example (LIMIT):**
```typescript theme={null}
const pair = await sdk.market.getTradingPairBySymbol('BTC/USDC');
const fees = await sdk.fees.simulateFees({
tradingPairId: pair.id,
side: "BUY",
price: "50000.00",
quantity: "0.1"
});
console.log(`Order value: $${fees.notional}`);
console.log(`Total fees: $${fees.totalTakerFees}`);
console.log(`You will pay: $${fees.takerTotalPayment}`);
```
**Example (MARKET with slippage):**
```typescript theme={null}
const fees = await sdk.fees.simulateFees({
tradingPairId: pair.id,
side: "BUY",
price: "50000.00",
quantity: "0.1",
orderType: "MARKET",
slippageToleranceBps: 1000 // 10% slippage buffer (matches the price band)
});
console.log(`Lock amount: $${fees.buyOrderLockAmount}`);
console.log(`Max affordable quantity: ${fees.maxQuantity}`);
console.log(`Slippage applied: ${fees.slippageToleranceBps} bps`);
```
## Fee Tier
Look up your current volume-based fee tier and a pair's full schedule.
Return the authenticated caller's current fee tier, rolling 14-day volumes, and the requested pair's six-row fee schedule. Requires authentication.
Fee tiers are volume-based and recalculated daily from a trailing 14-day weighted volume (`14d perp + 2.5 × 14d spot`), summed across every application the wallet trades through, so the tier and the volumes below are the same from any application. See [Fees](/fees) for the tier tables.
**Parameters:**
* `tradingPairId`: string - Trading pair UUID whose fee schedule to return
**Returns:**
* `currentTierLevel`: number - Resolved tier, `1` (lowest volume) through `6` (highest), from the higher of `weightedVolume14d` and any volume floor set on the wallet (a floor that is set or changed counts once it applies to the wallet's fills, usually within a minute, and likewise for an account the wallet opens in a new application; it stops counting at once when removed or expired)
* `weightedVolume14d`: string - Weighted 14-day volume (perp + 2.5× spot) the tier resolves from, unless a higher volume floor is set on the wallet, in whole USD (e.g. `"5000000"` = \$5 M)
* `spotVolume14d`: string - Rolling 14-day spot volume, in whole USD
* `perpVolume14d`: string - Rolling 14-day perp volume, in whole USD
* `volumeToNextTier`: string | null - Additional weighted volume needed to reach the next tier, counted from the volume the current tier resolved from; omitted at tier 6
* `nextTierLevel`: number | null - Next tier level; omitted at tier 6
* `feeSchedule`: FeeTierScheduleRow\[] - The pair's six rows, ascending by tier. Each row: `tierLevel` (number), `minVolumeThreshold` (string — weighted 14-day volume floor in quote/USD units), `makerFeeBps` (string — human bps, negative = rebate), `takerFeeBps` (string — human bps)
**Example:**
```typescript theme={null}
const pair = await sdk.market.getTradingPairBySymbol('BTC/USDC');
const tier = await sdk.fees.getMyFeeTier({ tradingPairId: pair.id });
console.log(`Current tier: ${tier.currentTierLevel}`);
console.log(`14d weighted volume: ${tier.weightedVolume14d}`);
if (tier.nextTierLevel != null) {
console.log(`${tier.volumeToNextTier} more volume to reach tier ${tier.nextTierLevel}`);
}
for (const row of tier.feeSchedule) {
console.log(`Tier ${row.tierLevel}: maker ${row.makerFeeBps} bps, taker ${row.takerFeeBps} bps`);
}
```
# Vault
Source: https://docs.0xmonaco.com/sdk/typescript/vault
Deposit and withdraw tokens
```typescript theme={null}
import { MonacoSDK } from "@0xmonaco/core";
// Initialize SDK with wallet client (see Initialization guide)
// Get asset ID from trading pair
const pair = await sdk.market.getTradingPairBySymbol('BTC/USDC');
const baseAssetId = pair.baseAssetId; // BTC asset ID
const quoteAssetId = pair.quoteAssetId; // USDC asset ID
// Vault operations (all use assetId - UUID)
await sdk.vault.deposit(quoteAssetId, amount, autoWait?); // deposit to spot (default)
await sdk.vault.deposit(quoteAssetId, amount, autoWait?, "margin"); // route directly to margin collateral
await sdk.vault.withdraw(quoteAssetId, amount, autoWait?); // withdraw from spot (default)
await sdk.vault.withdraw(quoteAssetId, amount, autoWait?, "margin"); // withdraw directly from parent margin collateral
await sdk.vault.retryWithdrawal(withdrawalIndex, autoWait?);
await sdk.vault.getVaultAddress();
// Low-level withdrawals (returns executable calldata WITHOUT broadcasting)
await sdk.withdrawals.initiateWithdrawal({ assetId, amount, destination });
await sdk.withdrawals.getWithdrawal(withdrawalIndex);
await sdk.withdrawals.listPendingWithdrawals({ pageSize?, pageToken? });
// Registered deposit addresses: auto-sweep ERC20 deposits into the rollup
await sdk.sweeper.register({ depositTarget? }); // identity comes from the session
await sdk.sweeper.getChains(); // public: which chains and assets are swept (an unlisted token is not swept)
// Token approvals (also use assetId - UUID)
await sdk.vault.approve(quoteAssetId, amount, autoWait?);
await sdk.vault.getAllowance(quoteAssetId);
await sdk.vault.needsApproval(quoteAssetId, amount);
```
## Vault Operations
Deposit tokens into vault. Uses asset ID (UUID) from trading pair.
**Parameters:**
* `assetId`: string - Asset UUID (from trading pair)
* `amount`: bigint - Token amount in smallest unit
* `autoWait?`: boolean - Wait for on-chain confirmation (default: true)
* `target?`: `"spot" | "margin"` - Destination ledger (default: `"spot"`)
* `"spot"` — credits the spot wallet (unchanged behavior)
* `"margin"` — routes the deposit straight into the parent margin account's collateral, skipping the separate `transferCollateralToParentMarginAccount` step. Deposits that cannot be routed to margin (e.g. unsupported asset) fall back to spot automatically.
* `options?`: `DepositOptions` - Deposit options. `asErc20: true` deposits the chain's wrapped-native asset (e.g. WSEI) as its ERC20 through approve + `depositERC20`, instead of the native-coin default below; it is ignored for every other asset. The ERC20 path is not auto-approved — `deposit` throws a `ContractError` (`code === "CONTRACT_ERROR"`) with `revertReason === "APPROVAL_REQUIRED"` if the allowance is short, so `approve` (or `needsApproval` with the same option) first.
**Example:**
```typescript theme={null}
const pair = await sdk.market.getTradingPairBySymbol('BTC/USDC');
const assetId = pair.quoteAssetId; // USDC
// Standard spot deposit (unchanged)
await sdk.vault.deposit(assetId, amount);
// Route directly into margin collateral
await sdk.vault.deposit(assetId, amount, true, "margin");
```
**Wrapped-native assets deposit as native coin.** When the asset is the chain's wrapped-native token (e.g. WSEI) — the `wrappedNative: true` flag on its [balance row](/developers/account-balances#balance-model) — `deposit` routes to the vault's payable `depositNative` and sends `amount` as `msg.value`; the vault wraps it on entry. **No ERC20 approval is required or checked** for these assets, so you can skip the `approve` / `needsApproval` step. Every other asset keeps the approve + `depositERC20` flow. Withdrawals of a wrapped-native asset pay out native coin (the vault unwraps), decided server-side with no request change.
To deposit the wrapped-native asset from a wallet that already holds the wrapped ERC20 itself, pass `{ asErc20: true }` as the fifth argument: `deposit` then takes the standard approve + `depositERC20` path (not `depositNative`), and `needsApproval(assetId, amount, { asErc20: true })` reads the real allowance instead of returning `false`.
Withdraw tokens from vault. Posts to `/api/v1/withdrawals`, waits for merkle-proof-backed `executeWithdrawal(...)` calldata, then submits the transaction on-chain.
**Parameters:**
* `assetId`: string - Asset UUID (from trading pair)
* `amount`: bigint - Token amount in smallest unit
* `autoWait?`: boolean - Wait for on-chain confirmation (default: true)
* `source?`: `"spot" | "margin"` - Source ledger (default: `"spot"`)
* `"spot"` — debits the spot wallet
* `"margin"` — directly debits withdrawable collateral from the parent margin account
When `autoWait` is `true` (default), the call waits for the withdrawal's on-chain merkle-proof calldata before submitting. The default wait timeout is **1 minute** (60 seconds), polling every 5000 ms. Override these via `WithdrawalRetryOptions { pollIntervalMs?: number; timeoutMs?: number }`.
When `autoWait` is `false`, the call returns immediately after the withdrawal is registered — it does **not** poll for the merkle proof. Use this when you want to initiate the withdrawal and complete the on-chain step later:
```typescript theme={null}
const { withdrawalIndex, status } = await sdk.vault.withdraw(assetId, amount, false);
// status === 'awaiting_proof' — proof not yet available, no transaction submitted
// Later, once the proof is confirmed:
await sdk.vault.retryWithdrawal(withdrawalIndex);
```
**Returns:** `WithdrawResult`:
* `withdrawalIndex`: number — the index allocated by the matching engine. Store this — you'll need it if the on-chain submission fails and you want to retry
* `hash?`: string — transaction hash; absent when `autoWait` is `false`
* `status`: string — `"awaiting_proof"` when `autoWait` is `false`; transaction status otherwise
Re-submit a withdrawal whose on-chain transaction failed or was dropped. Fetches the same merkle-proof-backed calldata for that `withdrawalIndex` and resubmits it. Does not allocate a new withdrawal slot — safe to call multiple times.
**Parameters:**
* `withdrawalIndex`: number — returned by the original `withdraw()` call
**Returns:** Same `WithdrawResult` shape as `withdraw()`.
Get the vault contract address for the current network.
**Returns:** Vault contract address (e.g., `0x1234...`)
**Use cases:**
* Add vault address to wallet for direct contract interaction
* Verify transactions on blockchain explorers
* Advanced integrations requiring direct contract access
**Example:**
```typescript theme={null}
const vaultAddress = await sdk.vault.getVaultAddress();
console.log("Vault contract:", vaultAddress);
// Example: 0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb
```
**Network-specific:**
* Staging (testnet): Returns Atlantic-2 vault address
## Low-Level Withdrawals
The `sdk.withdrawals` API exposes the raw withdrawal calldata flow. Unlike
`sdk.vault.withdraw` — which waits for the merkle-proof-backed calldata **and**
broadcasts the transaction on-chain — `sdk.withdrawals` returns the ABI-encoded
`executeWithdrawal(...)` calldata **without** broadcasting. Use it when you want
to submit the transaction yourself (custom signer, relayer, batching, or
inspection).
Initiate a withdrawal. Authenticated (master accounts with the withdraw permission). Debits the caller's balance via the matching engine and allocates a withdrawal index. Does **not** submit on-chain.
**Request:**
* `assetId`: Asset UUID to withdraw
* `amount`: Raw token amount in the smallest unit, as a stringified positive integer
* `destination`: On-chain address that will receive the withdrawal (EVM, 0x-prefixed)
The destination must differ from the configured vault contract address; using that address returns `400` before the balance is debited.
**Throws:** `400` for an invalid amount, address or asset, including the vault as destination; `401` without valid authentication; `403` without withdrawal permission or for an unsupported account type; `404` when the asset is absent; `429` when the movement budget is exhausted; `500` on an internal failure; `503` when the matching engine is unavailable or maintenance blocks the write. Do not initiate a second withdrawal after an ambiguous transport failure; reconcile the original first.
**Returns:** `WithdrawalResponse`:
* `withdrawalIndex`: number — matches `executeWithdrawal.index` on-chain
* `vaultAddress`: string — vault contract the calldata is submitted to
* `calldata`: string — ABI-encoded `executeWithdrawal(...)` calldata; empty until the merkle proof is available
Re-fetch a previously-initiated withdrawal's executable calldata. Public — no auth required.
**Parameters:**
* `withdrawalIndex`: number — returned by `initiateWithdrawal()`
**Returns:** Same `WithdrawalResponse` shape as `initiateWithdrawal()`.
List the caller's withdrawals that are still awaiting on-chain root confirmation, newest first. Authenticated and scoped to the caller's own account. An entry here has no calldata yet: once it is confirmed it leaves this list, and `getWithdrawal()` returns its calldata.
**Parameters:** all optional
* `pageSize?`: number — `1`–`1000`, default `20`. A larger value is rejected, not clamped
* `pageToken?`: string — opaque cursor: omit it (or send `""`) to start from the newest pending withdrawal, then pass the prior response's `nextPageToken` to continue. The walk seeks on `(createdAt, withdrawalIndex)` descending, so a withdrawal requested mid-walk cannot make the walk repeat or drop a row
There is no `page`: the SDK rejects a leftover one with a `ValidationError` before a request is sent, and the server answers `400`.
**Returns:** `ListPendingWithdrawalsResponse`:
* `withdrawals`: `PendingWithdrawal[]` — each with `withdrawalIndex`, `assetId`, `assetSymbol`, raw `amount` (smallest unit, stringified integer), `destination`, `status` (always `"pending"`) and RFC 3339 `createdAt`
* `total`, `pageSize`, `totalPages`: number — exact counts
* `nextPageToken`: string — pass back as `pageToken`; empty once the walk is exhausted. This listing is one ordered read, so a page shorter than `pageSize` is a definitive end
**Throws:** `400` for invalid pagination; `401` without valid authentication; `429` when the enforced read budget is exhausted; `500` on an internal failure.
## Registered Deposit Addresses
Instead of the `approve` → `deposit` flow above, you can give a user a **stable, deterministic deposit address** and have deposits of the watched assets swept into the rollup automatically. Only the assets `sweeper.getChains` (below) lists are indexed — the transfer indexer filters logs to those token addresses — so an unlisted ERC20 sent to the address is not swept. Every `(application, user, deposit target)` triple maps to one such address; registering the triple makes the backend monitor it and forward a watched asset once its accumulated balance reaches that asset's minimum, crediting the user under the application — no per-deposit signature or transaction from your app.
Register a deposit address so it is monitored and swept. **Requires authentication** — call `sdk.login()` first.
Both halves of the identity come from the session: the application is the one `sdk.login()` was called with, and the credited address is the session's own. Neither is a request field, so this can only ever register the caller's own address under their own application — and the body carries nothing but the target.
**Request — `RegisterSweeperRequest`** (optional; omit it entirely for a spot address):
* `depositTarget?`: `"spot" | "margin"` — which ledger swept deposits **ask** to be credited to. Defaults to `"spot"`.
* `"spot"` — credits the spot wallet (unchanged behavior).
* `"margin"` — asks for the parent margin account's collateral, skipping the separate `transferCollateralToParentMarginAccount` step. A request, not a guarantee — see the note below.
**Returns — `RegisterSweeperResponse`:**
* `status`: `"registered"` (the triple was added by this call) or `"already_registered"` (it was known before).
* `sweeperAddress`: string — the deposit address for this triple. Stable, and valid to publish and fund **before** any contract exists there.
* `initialSweepCheck`: boolean — whether the one-time registration balance check has completed. `false` right after a fresh registration; it runs in the background and flips once every pre-existing balance has been swept.
Idempotent and safe to retry within the current forwarder-based derivation: re-registering an existing triple returns `already_registered` with the same derived address. A completed initial check is never repeated; if `initialSweepCheck` is still `false`, registration schedules it again, while an already-running check is deduplicated.
**Pre-v1.0.53 registrations need an operator migration.** Calling register for one returns and monitors the new forwarder-derived address in the running resolver, but does not replace that row's stored address; after a resolver restart, warmup reloads only the old value. Pause deposits for that registration and do not display or fund the new address until Monaco confirms its stored row was migrated. Re-registration alone is not restart-durable.
**Example:**
```typescript theme={null}
const { status, sweeperAddress, initialSweepCheck } = await sdk.sweeper.register();
console.log(`Deposit address (${status}): ${sweeperAddress}`);
// Show it to the user and let them fund it — deposits of the watched assets
// (the ones `sweeper.getChains` lists) sweep in automatically.
// Or ask for the margin address instead — a different address:
const margin = await sdk.sweeper.register({ depositTarget: "margin" });
```
**Errors:** `400` (a `depositTarget` that is neither `spot` nor `margin` — the SDK rejects it before sending — or a body that still carries the removed `clientId` or `userAddress`, which unknown-field rejection turns into a `400` rather than ignoring it; that one only reaches you from an untyped or hand-rolled request, since the typed request has no such fields), `401` (no or invalid session), `403` (the session's application is no longer active), `429` (the service-wide admission limit on *new* registrations — they create durable state, so they are bounded across all callers; re-registering an existing address is never limited, and a refused call stores nothing. The body's `retryAfter` is the limiter's own remaining wait in seconds, not a fixed window and not the 24-hour default other limits use — the bucket refills continuously, so it is usually short. Honor the value rather than assuming one), `500` (a server-side failure while registering), and `503` (the sweeper could not complete the registration — several unrelated conditions share this status, from missing configuration to an unreachable chain or store). Treat a `503` as an **unknown** outcome, not a negative one: the registration is stored before the reply is composed, so a failure reported after that write is indistinguishable from one reported before it, and the status alone never tells you which happened. Retrying is always safe — registration is idempotent, and the retry answers `registered` or `already_registered` according to whether the first attempt had landed. Both are success and both carry the same address, so re-register rather than reporting failure, and accept either status.
List the chains and assets the sweeper watches. Public — it takes no arguments and needs no session, so a deposit page can show the supported set before the user logs in.
Each `SweeperChain` carries `chainId` (the EVM chain id, 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 `symbol`, the `tokenAddress` to send on that chain, `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.
**Example:**
```typescript theme={null}
const { chains } = await sdk.sweeper.getChains();
for (const chain of chains) {
console.log(`${chain.name} (${chain.chainId})${chain.hub ? " — settlement hub" : ""}`);
for (const asset of chain.assets) {
console.log(` ${asset.symbol} ${asset.tokenAddress} min ${asset.minSweepRaw}`);
}
}
```
The listing is all-or-nothing: rather than publish a partial set, the resolver refuses the whole call whenever any configured chain cannot be advertised. How current it is 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 also serves the last answer from a short cache. The two intervals stack — at the defaults the resolver refreshes its asset registry every 60 seconds and the gateway caches the resolver's answer for 60 seconds — so a change can take close to two minutes to appear. Treat the listing as recent, not live.
**Errors:** `500` (a server-side failure) and `503` — both safe to retry. There is no `401`: the call is unauthenticated. The `503` covers more than a missing configuration: a configured chain disabled by its own configuration, a chain whose id the resolver has not yet verified against its RPC (an unreachable RPC included — until it verifies, no indexer is watching that chain, so it must not be advertised), a chain whose RPC reports an id outside the JSON-safe range, and a resolver that could not answer at all.
**Bind a deposit to the exact `(chainId, tokenAddress)` pair `getChains` returns.** Never resolve a token by `symbol` alone: the same symbol can front a different contract on another chain, and a deposit sent to the wrong contract or the wrong chain is not recoverable. Show the user the chain and the token address from the same entry you took the minimum from.
**One registration covers every listed chain.** The address `register` returns is identical on, and watched on, every chain `getChains` lists, so there is nothing chain-specific to pass at registration. Use `getChains` to tell a depositor where they may send and which token address and minimum applies there — not to choose where to register.
**The deposit target is part of the address.** `"spot"` and `"margin"` derive **two different deposit addresses** for the same application and user, and each address carries its own target. Sending to the spot address never credits margin, whichever target you registered most recently. Show the user the address returned by the registration whose target you want, and keep the two apart in your UI.
**`"margin"` is a routing request, not a guarantee.** A deposit that arrives at a margin address but cannot be routed to collateral — an unsupported collateral asset, or a margin account that fails validation — is credited to the **spot wallet** instead, exactly as it is for a direct `deposit(..., "margin")`. Funds are never stranded, but do not treat a margin deposit address as proof the credit landed in collateral: read the balance back (`getParentMarginAccountSummary`) rather than assuming.
Deriving the address off-chain: `@0xmonaco/contracts` exports `predictSweeperAddress({ factory, forwarder, clientId, user, depositTarget? })`, which returns the same EIP-55 checksummed address without an RPC round-trip. The `forwarder` is the fixed delegation target every clone points at — read it once from `factory.forwarder()`; the sweeper implementation is deliberately not a derivation input, so logic upgrades never move addresses. It still takes `clientId` explicitly — off-chain derivation has no session to read it from, and it is your own application's id — useful to display or fund an address before registering. Pass the same `depositTarget` you register with, or the prediction points at the other ledger's address. `encodeDepositApplicationData(clientId, target)` exposes the underlying encoding, and the `SWEEPER_ABI` / `SWEEPER_FACTORY_ABI` exports cover direct contract interaction.
**`SweepParams` binds a sweep to an exact amount.** The executor-supplied struct the ABIs encode for `sweep(token, params)` / `sweepWithParams` leads with `amount`, ahead of `minSwapOut` and `minBridgeOut` — the exact balance the quote was computed for. The sweep moves exactly that much and leaves any excess, such as a deposit that landed after the quote, for the next re-quoted sweep, so the minimums always cover the full swept amount and a later arrival can never dilute them. A balance that has fallen below the quoted amount reverts with `StaleQuote` rather than sweeping less against minimums quoted for more: re-quote and retry. `amount: 0n` binds nothing and sweeps the execution-time balance, and is accepted **only** on the direct CCTP route, whose burn is exact-amount and carries no minimum that could go stale — every other route (Stargate, swap-then-bridge, and the hub unswap) rejects the sentinel with `QuoteNotBound`. A sweep against an empty balance reverts with `NothingToSweep`. Both errors are declared on `SWEEPER_ABI` only — `SWEEPER_FACTORY_ABI` carries the new `amount` field on `sweepWithParams` but declares neither — so decode factory-route reverts with both ABIs, not the factory's alone. Address derivation does not read `SweepParams`, so `predictSweeperAddress(...)` is unaffected by the struct's shape.
## Token Approvals
Approve vault to spend tokens. Uses asset ID (UUID).
Check current token allowance. Uses asset ID (UUID).
Check if approval is needed before deposit. Uses asset ID (UUID). Returns `false` for a `wrappedNative` asset (e.g. WSEI) without reading the on-chain allowance — its deposit goes through the payable `depositNative`, which consumes no allowance. Pass `{ asErc20: true }` to check the real allowance for depositing that wrapped-native asset as its ERC20 (matching a `deposit` with the same option).
## Deprecated Methods
**vault.getBalance() has been deprecated** and no longer functions as of v0.5.5. Use the Profile API instead.
The `vault.getBalance()` method is still exported for compatibility but always throws an `APIError` with `statusCode: 410` client-side, without sending a request. For balance queries, use the Profile API which provides more detailed information:
```typescript theme={null}
// ❌ Deprecated (throws error in v0.5.5+)
const balance = await sdk.vault.getBalance(assetId);
// ✅ Use Profile API instead
const balance = await sdk.profile.getUserBalanceByAssetId(assetId);
console.log('Available:', balance.availableBalance);
console.log('Locked:', balance.lockedBalance);
console.log('Total:', balance.totalBalance);
```
**Why this changed:**
* Profile API provides more detailed balance information (available, locked, total)
* Consolidates balance queries with other profile data
* Improves performance with backend optimizations
**Get all balances:**
```typescript theme={null}
const { balances } = await sdk.profile.getUserBalances({ pageSize: 100 });
balances.forEach(b => {
console.log(`${b.symbol}: ${b.totalBalance} (${b.availableBalance} available)`);
});
```
See [Profile API documentation](/sdk/typescript/profile) for more details.
## Getting Asset IDs
Asset IDs (UUIDs) are obtained from trading pairs. Each trading pair has `baseAssetId` and `quoteAssetId`.
```typescript theme={null}
// Get asset IDs from trading pair
const pair = await sdk.market.getTradingPairBySymbol('BTC/USDC');
// pair contains:
// - baseAssetId: UUID for the base token (e.g., BTC)
// - quoteAssetId: UUID for the quote token (e.g., USDC)
// - baseTokenContract: Contract address for base token
// - quoteTokenContract: Contract address for quote token
```
| Method | Parameter | Description |
| - | - | - |
| `deposit()` | `assetId` (UUID) | Monaco asset identifier |
| `withdraw()` | `assetId` (UUID) | Monaco asset identifier |
| `approve()` | `assetId` (UUID) | Monaco asset identifier |
| `needsApproval()` | `assetId` (UUID) | Monaco asset identifier |
| `getAllowance()` | `assetId` (UUID) | Monaco asset identifier |
# WebSockets
Source: https://docs.0xmonaco.com/sdk/typescript/websockets
Real-time order updates and market data streams
The Monaco SDK provides a unified WebSocket client for real-time data feeds including order events, OHLCV candlestick updates, orderbook changes, and trade executions.
## WebSocket Client
The SDK exposes a single WebSocket client with functional subscription methods:
```typescript theme={null}
sdk.ws.orders(pairId, tradingMode, handler) // REQUIRES AUTH
sdk.ws.userOrders(handler) // REQUIRES AUTH
sdk.ws.orderbook(pairId, tradingMode, magnitude, qMode, handler) // PUBLIC
sdk.ws.ohlcv(pairId, tradingMode, interval, handler, priceType?, onSnapshot?) // PUBLIC
sdk.ws.trades(pairId, handler, onSnapshot?) // PUBLIC
sdk.ws.movements(handler) // REQUIRES AUTH
sdk.ws.balances(handler) // REQUIRES AUTH
sdk.ws.conditionalOrders(handler, pairId?) // REQUIRES AUTH
sdk.ws.positions(handler, pairId?, onSnapshot?, onValuation?) // REQUIRES AUTH
sdk.ws.liquidations(handler, pairId?) // REQUIRES AUTH
sdk.ws.account(handler) // REQUIRES AUTH
sdk.ws.marketStats(pairId, handler) // PUBLIC
sdk.ws.marketStatsAll(handler) // PUBLIC
sdk.ws.instruments(handler, pairId?) // PUBLIC
sdk.ws.twapOrders(handler, pairId?) // REQUIRES AUTH
sdk.ws.copy(handler, onSnapshot?) // REQUIRES AUTH
```
Each method returns an **unsubscribe function** for easy cleanup.
## Connection
The WebSocket auto-connects when the SDK is initialized. You can also manually manage the connection:
```typescript theme={null}
// Check connection status
const status = sdk.ws.getStatus(); // "connecting", "connected", "reconnecting" or "disconnected"
// Manual connect/disconnect
await sdk.ws.connect();
sdk.ws.disconnect();
```
### Auto-Connect Behavior
By default, the WebSocket connection is established automatically when you initialize the SDK:
```typescript theme={null}
const sdk = new MonacoSDK({
walletClient,
network: "staging",
seiRpcUrl: "https://evm-rpc-testnet.sei-apis.com", // public testnet RPC — use a private provider in production
});
// WebSocket is already connecting in the background
// Connection errors are logged but don't block SDK initialization
```
The auto-connect process:
* Runs asynchronously without blocking SDK initialization
* Logs connection errors to console but doesn't throw
* Allows you to call SDK methods immediately while connection is established
* Automatically retries if initial connection fails
### Manual Connection Management
You may want to manually control the WebSocket connection in these scenarios:
**1. Delayed Connection**
If you don't need real-time data immediately:
```typescript theme={null}
const sdk = new MonacoSDK({
walletClient,
network: "staging",
seiRpcUrl: "https://evm-rpc-testnet.sei-apis.com",
});
// Disconnect auto-connection
sdk.ws.disconnect();
// Connect later when needed
await sdk.ws.connect();
```
**2. Connection Recovery**
Handle connection errors explicitly:
```typescript theme={null}
try {
await sdk.ws.connect();
console.log("WebSocket connected successfully");
} catch (error) {
console.error("WebSocket connection failed:", error);
// Implement retry logic or notify user
}
```
**3. Resource Management**
Disconnect when not in use to save resources:
```typescript theme={null}
// When user navigates away or logs out
sdk.ws.disconnect();
// Reconnect when needed
await sdk.ws.connect();
```
### Connection Methods
Manually connect to the WebSocket server.
**Returns:** Promise that resolves when connected, rejects on error.
**Note:** Does nothing if already connected. Safe to call multiple times.
Disconnect from the WebSocket server.
**Returns:** void (synchronous)
**Note:** Does nothing if already disconnected. Clears all subscriptions.
Get current connection status.
**Returns:** One of: `"connected"`, `"connecting"`, `"disconnected"`, `"reconnecting"`. Inside `onStatusChange`, it returns the status being reported. See [Connection status](#connection-status).
### Connection status
Pass `onStatusChange` in the SDK's `ws` config (or to `createMonacoWebSocket`) to hear every status change. The connection reports four statuses:
| Status | When it fires |
| - | - |
| `"connecting"` | A connection starts opening outside a retry cycle: the SDK's first connect at construction, a `connect()` after a terminal `"disconnected"`, or a fresh session's reconnect after [session loss](#session-loss-close-code-1008). It comes before any other status for that connection. |
| `"connected"` | The connection opened. |
| `"reconnecting"` | A retry is scheduled, once per retry. It covers both a first connect that failed and a working connection that was lost. Retries do not report `"connecting"`: the status stays `"reconnecting"` until a retry connects, the retries run out, or you call `disconnect()`. |
| `"disconnected"` | Terminal: a clean close, an abnormal close with `autoReconnect: false`, retries exhausted (only with a finite `maxReconnectAttempts`), session loss, or `disconnect()`. |
Each call also receives an `info` object:
* `attempt`: the retry the status refers to. It is `0` for `"connecting"` and `"connected"` and `1`, `2`, … for `"reconnecting"`. For `"disconnected"` it is the number of retries scheduled in the outage before it, counting one still in backoff when you call `disconnect()`, and `0` after a clean close of a working connection.
* `hasConnected`: whether the client has reached the server at least once since it was created or since the last `disconnect()`.
* `nextRetryDelayMs`: on `"reconnecting"` only, the delay in milliseconds before that retry opens its connection.
`"reconnecting"` with `hasConnected: false` means the client has never reached the server. With the default unlimited retries it keeps trying, so use `info` to tell that apart from a lost connection:
```typescript theme={null}
const sdk = new MonacoSDK({
walletClient,
network: "staging",
seiRpcUrl: "https://evm-rpc-testnet.sei-apis.com",
ws: {
onStatusChange: (status, { attempt, hasConnected, nextRetryDelayMs }) => {
if (status === "reconnecting" && !hasConnected) {
showBanner(`Can't reach Monaco (attempt ${attempt}), retrying in ${Math.round((nextRetryDelayMs ?? 0) / 1000)}s`);
} else if (status === "reconnecting") {
showBanner(`Connection lost, reconnecting (attempt ${attempt})`);
} else if (status === "connected") {
hideBanner();
}
},
},
});
```
A handler that takes only `status` still works. An error thrown by the handler, or a rejection from an async handler, is logged to the console and does not affect the connection.
### Reconnection and resync
`sdk.ws` auto-reconnects with full-jitter exponential backoff (base 1s, capped at 30s) and retries indefinitely by default. `getStatus()` returns `"reconnecting"` while backing off and while a retry's connection is opening, and Ping/Pong heartbeats are handled automatically.
To react to reconnects, pass a `ws` config to the SDK — `new MonacoSDK({ ..., ws: { onStatusChange, onResync } })`. The constructor forwards it to the WebSocket client it builds internally, so you no longer need to drop to the low-level `createMonacoWebSocket(baseUrl, options)` factory (which remains available and additionally takes the session keypair). The same fields are accepted either way:
* `onStatusChange(status, info)` — fires on every status change, with the retry attempt and whether the client has ever connected. See [Connection status](#connection-status).
* `onResync({ code, slowClient })` — fires after each automatic reconnect, after sending authentication/subscription requests, without waiting for acknowledgements or snapshots. A channel with a [subscribe-time snapshot](#subscribe-time-snapshots) can deliver a fresh baseline over the socket on reconnect, so the REST refetch below is for the channels that have none — and the same `(id, version)` rule reconciles either source. Refetch REST snapshots and merge them into local state on `(id, version)`: order events, the `orders` snapshot frame, and the REST endpoints all carry the same counter, so keep applying events as they arrive and take the snapshot for an order when `snapshot.version >= local.version`, else keep local. Use `>=`, not `>` — on the stream equal versions mean the same sequencer step, not identical state (one step emits an `OrderPlaced` and then its fills; a batch shares one step across all items), and the REST row is that step's final state. Two things the resting snapshot cannot tell you, because they stopped resting during the gap: fills, which you recover from `sdk.profile.getUserTrades` (execution-time ordered, so a gap fill is at the top whatever the order's age — the order list is `createdAt`-ordered and would bury it); and cancels or expiries, signalled by an order being absent from the snapshot that local state still thinks is resting. Snapshot absence is not a tombstone — persistence may instead lag a newer local resting event — so retain the local row and resolve it with `sdk.trading.getOrder(orderId)`, then apply the same version rule. Treat an absent `version` as unknown, never zero, and fall back to buffer-and-replay for those rows. The [market-maker runbook](/developers/market-maker-runbook#4-reconnect-and-resync) has the full recipe. `slowClient` is `true` when the server shed the connection with close code `1013` (the client fell behind — expect a gap).
* `onError({ code, message, channel })` — fires for every `Error` frame the server sends; `channel` is present only on a per-channel failure. See [Server error frames](#server-error-frames).
* `onReauthenticationRequired({ code, message, closeCode })` — fires once when the connection lost its authentication for good and the SDK stopped reconnecting. See [Session loss (close code 1008)](#session-loss-close-code-1008).
* `autoReconnect`, `maxReconnectAttempts`, `reconnectBaseDelayMs`, and `connectionTimeoutMs` toggle reconnection and tune the retry cap, backoff base, and per-attempt timeout. The retry cap counts per outage: a `connect()` after the retries run out starts a fresh budget.
### Subscribe-time snapshots
Most channels deliver their **current state** once, as a `Snapshot` frame the server injects after the subscribe ack and before that subscription's first live event. Pass an `onSnapshot` callback to receive it as one array — a complete baseline, straight off the socket, with no REST read.
```typescript theme={null}
const unsubscribe = sdk.ws.balances(
(event) => applyBalanceUpdate(event.data), // live diffs
(balances) => setBalances(balances), // complete current state, once
);
```
`onSnapshot` is always the **last** argument, and always optional — existing calls that omit it are unaffected. On a channel that also takes a `tradingPairId` filter, it follows the filter:
```typescript theme={null}
// All pairs, with a baseline
sdk.ws.positions(handler, undefined, (positions) => setPositions(positions));
// One pair, with a baseline
sdk.ws.positions(handler, pair.id, (positions) => setPositions(positions));
```
Five rules govern it:
* **The array is the whole channel, once.** It is not fanned through the event handler — snapshot rows and live events are different shapes, so a handler that reduced both would apply the same state twice.
* **Empty means empty.** A user with no open positions gets `onSnapshot([])`, not a skipped call, so you can clear stale state rather than guess.
* **It fires again after every reconnect.** The resubscribe is a new subscription server-side, so a fresh snapshot follows. Apply it according to that channel's semantics: complete maps such as positions or instruments can replace their prior set; the persisted resting-orders snapshot must be version-ranked against held events, with omitted resting rows resolved by `getOrder` rather than deleted on absence alone.
* **It is scoped to the exact channel you subscribed to.** Two pair-scoped subscriptions each receive only their own baseline.
* **It is withheld rather than truncated.** If any row of the frame cannot be parsed, nothing is delivered and your existing state stands. The array is contractually complete, so a short one would have you delete whatever the unparseable row described — and a resting order or an unchanged market has no later event to undo that.
**One baseline per subscription, not per call.** The client opens one wire subscription per channel however many handlers you register on it, and the server snapshots only genuinely new subscriptions. A second call made while that subscription's first snapshot is still pending joins the handler set and receives the upcoming frame. A call added after the frame arrived has no cached baseline and waits until the next reconnect. `instruments` is the exception — it caches its baseline and replays it locally, inactive markets included, so the replay matches what a real snapshot would carry. Subscribe once per channel and fan out in your own code, or read the baseline over REST for an extra consumer added later.
This is the cheaper half of [reconnection and resync](#reconnection-and-resync): for every channel below, the snapshot replaces the broad REST refetch that `onResync` would otherwise prompt. Orders can still require targeted `getOrder` calls for locally resting rows the persisted snapshot omits.
| Channel | `onSnapshot` receives | Status |
| - | - | - |
| `orders` / `userOrders` | `OrderSnapshotItem[]` — every resting order | Available |
| `balances` | `UserBalanceEventData[]` — every asset held | Available |
| `conditionalOrders` | `ConditionalOrderEventData[]` — every live conditional order | Available |
| `positions` | `PositionEventData[]` — every open position | Available |
| `instruments` | `InstrumentEventData[]` — the full config of every listed market, inactive ones flagged `isActive: false` | Available |
| `account` | `AccountEventData[]` — the margin health of every margin account you own **in the authenticated application**, including flat ones; rows omit `sequence`. NOT a user-wide replacement set: live delivery is per user and wider, so merge per `marginAccountId` | Available |
| `liquidations` | `LiquidationEventData[]` — every in-flight (`UNRESOLVED`) liquidation record you own; account-scoped, so each row's `positionId`/`tradingPairId`/`liquidationPrice`/`markPrice` is `undefined` (the SDK parses the wire `null` to `undefined`) and `riskBucketState` is `Liquidating`. A pair-scoped `liquidations:{pair}` subscription baselines empty — use the bare `liquidations` channel | Available |
| `twapOrders` | `TwapOrderEventData[]` — every live (`PENDING`/`ACTIVE`) TWAP parent | Available |
| `copy` | `CopyRelationshipEventData[]` — every live (`ACTIVE`/`CLOSE_ONLY`) follow, each with `change: "snapshot"` | Available |
| `marketStats` | `MarketStatsData[]` — one element, that market's latest complete stats frame; the same shape as the live per-market event | Available |
| `marketStatsAll` | `MarketStatsSummary[]` — one summary per active market with retained data, deterministic by trading-pair id; the same shape as an element of the live all-markets frame's `markets` list | Available |
| `trades` | `TradeEventData[]` — the market's newest 50 trades, newest first; the same shape as the live event's `data`. A recent-history baseline, not complete state: it can overlap the first live trades or miss one executed just before you subscribed, so dedupe on `tradeId` | Available |
| `ohlcv` | `OHLCVSnapshotCandle[]` — the subscribed series' newest 350 candles, oldest first; the live `Candlestick` without `s`. A recent-history baseline, not complete state: its newest candle can be partial or missing, so upsert live candles by `T`. A `"*"` price type gets none | Available |
`orderbook` and `movements` take no `onSnapshot`: they stream events rather than state. For `orderbook` the first live frame is already the full book; for `movements`, use the REST history read.
Every row is the channel's own terminal-state payload — the same shape as its live event's `data` — with one exception. `orders` snapshot rows describe a **resting order**, not a change, so `OrderSnapshotItem` carries `status` and the cumulative fill totals and carries none of the per-fill metadata (`tradeId`, `executionPrice`, `lastFillQuantity`, `role`, `tradesCount`, and the `fee`, `monacoFee` and `builderFee` fields with their `Raw` twins):
```typescript theme={null}
interface OrderSnapshotItem {
orderId: string;
userId: string;
applicationId: string;
tradingPairId: string;
tradingPair: string; // symbol, e.g. "BTC/USDC"
tradingMode: string; // "SPOT" | "MARGIN"
side: string; // "BUY" | "SELL"
orderType: string; // "LIMIT" | "MARKET"
price: string | null; // null for a market order
quantity: string;
quantityRaw: string;
filledQuantity: string;
filledQuantityRaw: string;
remainingQuantity: string | null;
remainingQuantityRaw: string | null;
status: string; // a resting status
timeInForce: string | null; // null on a MARKET order, which has none
averageFillPrice: string | null; // null before the first fill
createdAt: string; // ISO 8601 timestamp (UTC)
updatedAt: string | null;
marginAccountId: string | null;
positionId: string | null;
leverage: string | null;
reduceOnly: boolean;
postOnly?: boolean; // `true` when resting post-only; omitted, never `false`, otherwise
expirationDate?: string; // custom GTC expiry (RFC 3339), as `getOrder` returns it; omitted when none
triggerPrice?: string; // trigger price of an order a conditional placed; omitted otherwise
positionSide?: string; // deprecated derived label, as `getOrder` returns it; `side` is authoritative
clientOrderId?: string; // omitted, not null, when the order carried none
selfTradePreventionMode?: SelfTradePreventionMode | (string & {}); // the mode REQUESTED on the order; omitted when it carried none
parentOrderId?: string; // the TWAP parent of a child order, as `getOrder` returns it; omitted for any other order
version?: number; // sequencer step that last wrote this order; omitted when unknown
}
```
The `| null` fields are always present on the wire and carry `null` when unset; `postOnly`, `expirationDate`, `triggerPrice`, `positionSide`, `clientOrderId`, `selfTradePreventionMode`, `parentOrderId` and `version` are the ones the server omits outright. Each of them is the value `getOrder` returns for the same order, so a row read on reconnect equals the REST row. `positionId` is the order's position binding: a resting close or reduce-only order placed against an open position carries it from admission, before it fills, as described under the order events below.
`version` is the one field here that is not resting state. It is the same counter the live order events and the REST endpoints carry, and it is what a reconnecting client ranks this frame against — compare with `>=`, not `>`, and treat an absent value as unknown rather than zero. See [reconnection and resync](#reconnection-and-resync) for the full rule.
### Slow-client disconnect (close code 1013)
Every broadcast-ring gap that one of the socket's subscriptions owns closes the whole socket with code `1013` and reason `slow consumer: stream gapped, resync required`, including sockets holding only `positions`, `balances`, `account`, `instruments`, `orderbook`, `ohlcv`, or market-stat subscriptions. The ring is not key-aware: a lost terminal or isolated-key update need not have a later replacement. There is no separate JSON error frame for lag. A gap closes the socket only if one of its subscriptions owns it: `orders`, `userOrders`, `conditionalOrders`, `twapOrders`, `positions`, `balances`, `account`, `instruments`, `liquidations`, `copy`, and single-market `marketStats` subscriptions own every gap, while other channels own only gaps reaching events published after their `Subscribed` acknowledgement (see the [reference](/reference/websockets#slow-client-disconnect-close-code-1013)).
The server sends the same `1013` close when events may have been lost before they reached it (an engine drop under load, a failed publish, or its NATS connection dropping or falling behind). Every socket on that server holding a subscription is closed, whatever its channels, and recovers through the same reconnect and `onResync({ slowClient: true })` path.
With automatic reconnect enabled, `sdk.ws` opens a new socket, sends its authentication/subscription requests, and invokes `onResync` with `slowClient: true`. The callback does **not** wait for `Authenticated`, `Subscribed`, or `Snapshot` frames. Reconciliation is application code: the SDK does not automatically refetch REST state.
A reconnect starts a new live stream; it does not replay missed NATS events. Consume subscribe snapshots where supported (`balances`, `orders`, `userOrders`, `conditionalOrders`, `positions`, `instruments`, `account`, `liquidations`, `twapOrders`, `copy`, `marketStats`, `marketStatsAll`), reconciling complete sets and per-key revisions. The `trades` snapshot is the market's newest 50 trades, merged with live trades by `tradeId`, not a complete set. Other channels have no subscribe snapshot: explicitly refresh the relevant REST state/history and reconcile events arriving during that read. Quiet keys may never produce a fresh live baseline; a flat margin account is covered by the `account` snapshot, which includes it even though the live producer emits for it only when its collateral moves. See [the protocol recovery contract](/reference/websockets#slow-client-disconnect-close-code-1013) for snapshot limits, missing terminal updates, and additive-history gaps. Public trades and movements have no universal WS replay cursor or guarantee of complete recovery beyond their REST history/pagination limits.
### Session loss (close code 1008)
When the server finds that an authenticated connection's session was revoked or expired (or the account was gated), it sends an `Error` frame with code `SESSION_INVALID` and closes with `1008`. When it rejects the `Authenticate` handshake itself, it sends `AUTH_FAILED` and closes. Reconnecting with the same session would only be rejected again, so after either the SDK does **not** reconnect automatically. It:
1. forgets the rejected session keypair, so a later `connect()` does not replay it;
2. keeps your subscription handlers;
3. reports `"disconnected"` through `onStatusChange`;
4. calls `onReauthenticationRequired({ code, message, closeCode })` once.
Recover by obtaining a fresh session: `sdk.login(...)` (or `setSessionKeypair` on a client from `createMonacoWebSocket`) reconnects, authenticates with the new session, resubscribes every channel you still hold, and then fires `onResync` with the lost connection's close code so you can reconcile what was missed (also when `autoReconnect` is off). If you already supplied a new session before the close arrived — for example from `onError` — the SDK keeps it and reconnects with it instead of calling `onReauthenticationRequired`:
```typescript theme={null}
const sdk = new MonacoSDK({
...,
ws: {
onReauthenticationRequired: ({ code }) => {
// SESSION_INVALID: revoked/expired session. AUTH_FAILED: handshake rejected.
console.warn("WebSocket session lost:", code);
void sdk.login(clientId);
},
onResync: ({ code }) => {
void reconcileFromRest(code);
},
},
});
```
Supply the fresh session instead of calling `connect()` yourself first: a socket opened without a session re-sends every held channel unauthenticated, the server rejects the user-specific ones with `AUTH_REQUIRED`, and authenticating on that open socket afterwards does not resubscribe them (see `AUTH_REQUIRED` below).
The server also rejects a handshake with `AUTH_FAILED` when it could not verify the session because of an internal error; that case stops reconnecting too, and calling `login` again (or re-applying the same session with `setSessionKeypair`) retries it. Every other abnormal close keeps the backoff reconnect — including a `1008` for exceeding the [inbound message rate](/reference/websockets#inbound-message-rate-close-code-1008), which carries `MESSAGE_RATE_LIMIT` rather than `SESSION_INVALID`, and a `1013` [slow-client disconnect](#slow-client-disconnect-close-code-1013).
### Server error frames
The server reports failures as `Error` frames carrying a machine-readable `code` and a human-readable `message`. They match no subscription, so they reach no channel handler — the `ws` config's `onError` callback is how they reach your app.
A per-channel failure also carries `channel`: the channel string the SDK sent, exactly as sent (a string longer than 128 bytes, never a valid channel, is echoed as its first 128 UTF-8 bytes, cut at a character boundary). The `Subscribed` / `Unsubscribed` acknowledgements list only the channels a request changed, so a rejected channel is missing from the ack and reported by its own `Error` frame instead; `channel` is how you pair the two. The SDK sends every held channel in one `Subscribe` on reconnect, so it is also how you tell which one failed and act on that channel alone. Connection-wide failures have no `channel`.
```typescript theme={null}
// Declared before the SDK so `onError` can close over it, and reassigned
// whenever the subscription is (re-)created. The subscribe call itself has to
// come after `sdk` exists, which is why the handle starts undefined.
let unsubscribeOrders: (() => void) | undefined;
const sdk = new MonacoSDK({
...,
ws: {
onError: ({ code, message, channel }) => {
switch (code) {
case "SNAPSHOT_UNAVAILABLE":
// The subscription is live and diffs flow, but this channel's
// subscribe-time baseline is missing — refetch just that one from
// REST.
if (channel) void refetchSnapshot(channel);
break;
case "SESSION_INVALID":
// The connection is closing and will not reconnect on its own;
// onReauthenticationRequired follows. Recover there.
break;
case "AUTH_REQUIRED":
// The channel was subscribed before authenticating, so the server
// rejected it WITHOUT registering it. Re-authenticating is not
// enough on its own: the client still holds your handler, so it
// treats the channel as already subscribed and sends no new
// Subscribe. Drop the subscription and re-create it after
// authenticating — releasing the last handler for a channel is what
// lets the next subscribe re-send Subscribe.
void reauthenticate().then(() => {
unsubscribeOrders?.();
unsubscribeOrders = sdk.ws.orders(pairId, "SPOT", onOrder);
});
break;
default:
console.error("ws error", code, message, channel);
}
},
},
});
unsubscribeOrders = sdk.ws.orders(pairId, "SPOT", onOrder);
```
| Code | `channel` | Meaning |
| - | - | - |
| `AUTH_REQUIRED` | Yes | A user-specific channel was subscribed before authenticating. The subscription is **not** registered — re-subscribe after authenticating, not just re-authenticate |
| `AUTH_FAILED` | — | The `Authenticate` handshake was rejected; the connection closes after this frame and the SDK does not reconnect — see [Session loss](#session-loss-close-code-1008) |
| `CONNECTION_LIMIT` | — | The per-user concurrent-connection cap was reached; this connection closes after the frame |
| `SUBSCRIPTION_LIMIT` | Yes | The per-connection subscription-cardinality cap was reached adding `channel` |
| `INVALID_SUBSCRIPTION` | Yes | A subscribe or unsubscribe request named a channel that was malformed or failed validation |
| `INVALID_MESSAGE` | — | An inbound frame could not be parsed, or an unsupported frame type was sent |
| `SESSION_INVALID` | — | A previously-authenticated session became invalid (revoked, expired, or the account was gated); the connection closes with `1008` and the SDK does not reconnect — see [Session loss](#session-loss-close-code-1008) |
| `SNAPSHOT_UNAVAILABLE` | Yes | The subscribe-time snapshot for `channel` could not be read; the subscription is still active |
| `MESSAGE_RATE_LIMIT` | — | A per-connection inbound cap was exceeded: the message rate (default 20 messages/s sustained, burst 1,200; operator-configurable) or the byte budget, which refuses a burst of very large frames; the connection closes with `1008` after the frame. Batch subscriptions and back off before reconnecting |
Always keep a `default` branch: a code added server-side after your SDK version is delivered as-is rather than dropped.
### Error Handling
WebSocket connection errors are handled gracefully:
```typescript theme={null}
// Auto-connect errors are logged but don't throw
const sdk = new MonacoSDK({ ... });
// Even if WebSocket fails, SDK methods work normally
// Manual connect errors can be caught
try {
await sdk.ws.connect();
} catch (error) {
if (error.message.includes("connection refused")) {
console.error("WebSocket server unavailable");
} else if (error.message.includes("timeout")) {
console.error("Connection timeout - check network");
} else {
console.error("WebSocket error:", error);
}
}
```
### Best Practices
**1. Let Auto-Connect Work**
In most cases, let the SDK handle connection automatically:
```typescript theme={null}
// ✅ Simple and works in most cases
const sdk = new MonacoSDK({ ... });
sdk.ws.orders(pairId, "SPOT", handler);
```
**2. Manual Connect for Control**
Use manual connection when you need explicit control:
```typescript theme={null}
// ✅ Useful for apps with conditional real-time features
const sdk = new MonacoSDK({ ... });
sdk.ws.disconnect(); // Disable auto-connect
if (userWantsRealtime) {
await sdk.ws.connect();
sdk.ws.orders(pairId, "SPOT", handler);
}
```
**3. Cleanup on Unmount**
Always disconnect when component unmounts:
```typescript theme={null}
// ✅ React example
useEffect(() => {
sdk.ws.connect();
return () => sdk.ws.disconnect();
}, []);
```
**4. Monitor Connection Status**
Check status before subscribing:
```typescript theme={null}
// ✅ Wait for connection before subscribing
if (sdk.ws.getStatus() !== "connected") {
await sdk.ws.connect();
}
sdk.ws.orders(pairId, "SPOT", handler);
```
### WebSocket Types
```typescript theme={null}
/** Connection status */
type WebSocketStatus = "connected" | "connecting" | "disconnected" | "reconnecting";
/** Orderbook price quotation mode */
type OrderbookQuotationMode = "BASE" | "QUOTE";
// BASE: Prices in quote currency per base (e.g., USD per BTC for BTC/USD)
// QUOTE: Prices in base currency per quote (e.g., BTC per USD for BTC/USD)
/** Valid magnitude values for orderbook grouping */
const ALL_MAGNITUDES = [0.0001, 0.001, 0.01, 0.1, 1, 10, 100, 1000, 10000];
```
## Orders Channel
Subscribe to real-time order events for your account. **Requires authentication.**
```typescript theme={null}
// Authenticate first
await sdk.login(clientId);
// Get trading pair
const pair = await sdk.market.getTradingPairBySymbol("BTC/USDC");
// Subscribe to order events
const unsubscribe = sdk.ws.orders(
pair.id, // Trading pair ID (UUID)
"SPOT", // Trading mode
(event) => {
console.log("Event type:", event.eventType);
console.log("Order ID:", event.orderId);
}
);
// Later, to unsubscribe:
unsubscribe();
```
### Event Timeline
Events are emitted in this order:
1. **OrderPlaced** → Order validated, persisted, funds locked (taker only)
2. **Match Events** → Emitted after OrderPlaced
* Taker: `OrderMatched` / `OrderPartiallyFilled` / `OrderFilled`
* Each Maker: `OrderPartiallyFilled` / `OrderFilled`
3. **OrderCancelled** → For an `IOC` order's unfilled remainder after a partial fill (an `FOK` that can only partially fill for want of liquidity is **rejected**, not cancelled — its candidate fills are discarded). Also for a market, `IOC` or `FOK` remainder (or a margin limit remainder) the price band cut short, carrying `terminalReason` `PRICE_BAND`, or `SLIPPAGE_TOLERANCE` when the order's own `slippageToleranceBps` was tighter. Also, when a risk bucket enters `PENDING_LIQUIDATION`, one terminal `OrderCancelled` per resting order in that risk bucket, carrying `terminalReason` `LIQUIDATION`. And after the taker's own fan-out, **one terminal `OrderCancelled` per resting order swept by [self-trade prevention](/developers/trade#self-trade-prevention)** — sent to that order's owner, carrying `terminalReason` `SELF_TRADE_PREVENTION` and `stpCounterpartyOrderId`. A maker subscriber therefore sees a terminal frame for an order it never cancelled
4. **OrderRejected** → For orders rejected *after* acceptance (an IOC/FOK that cannot fill **for want of liquidity**; a `MARKET` order whose liquidity is invalidated by maker-risk revalidation). An IOC/FOK stopped by [self-trade prevention](/developers/trade#self-trade-prevention) instead is `OrderCancelled`, even at zero fills. A plain `MARKET` submit with no liquidity is rejected before acceptance and emits nothing.
5. **OrderExpired** → For GTC orders reaching expiration
### Taker vs Maker
Events contain different fields based on your role:
| Field | Taker | Maker |
| - | - | - |
| `role` | `"taker"` | `"maker"` |
| `tradesCount` | Number of fills | Always `1` |
| `referencePrice` | Yes | `null` |
| `bestPrice` / `worstPrice` | Yes | `null` |
| `actualSlippageBps` | Yes | `null` |
### Order Event Types
Every order event also carries `data.version?: number`, the same opaque, compare-only revision as REST reads and the `orders` subscription snapshot. Compare only states of the same order: higher means newer, gaps are normal, and absent means unknown, never zero. Merge a snapshot when `snapshot.version >= local.version`; it wins ties because several live events can share one sequencer step. See the [reconciliation recipe](/developers/market-maker-runbook#4-reconnect-and-resync).
Event value fields typed as `KnownAlias | (string & {})` preserve unfamiliar server strings; keep a `default` branch when narrowing them. Each named event variant keeps its closed `eventType` discriminant.
Every order event's `data` also carries an optional `clientOrderId?: string` (wire `client_order_id`) when the order was placed with one — echoed on placement, taker and maker fills, cancellation, rejection and expiry, so an event can be matched to the request that placed the order. A maker fill carries the resting order's own handle. It is omitted when the order carried none.
Every order event's `data` may also carry `origin?` (wire `origin`) — what placed the order: `"USER"`, `"LIQUIDATION"`, `"SYSTEM"`, `"ADL"` or `"COPY"` — and, on a `"COPY"` order, `copyRelationshipId?: string` (wire `copy_relationship_id`), the follow the [copy trading](/developers/copy-trading) engine placed it for (the same id as `Follow.followId` and the `copy` channel's `relationshipId`). An absent `origin` means **not reported**, never `"USER"`: producers fill it as they adopt it, and send `"USER"` explicitly once they do.
`selfTradePreventionMode?` is a **common order-event field**, not a cancellation-only one: the requested override rides the `OrderPlaced` acknowledgement, fills in both roles, `OrderRejected`, `OrderCancelled`, `OrderPartiallyCancelled`, `OrderExpired` and the `orders` snapshot frame. It echoes what the request carried and is absent when it carried none — it is never the resolved wallet or platform default, so absence does not mean `CANCEL_MAKER` was not applied.
A TWAP child order reports `parentOrderId?: string` (wire `parent_order_id`), the same id `getOrder` returns, so you can group a TWAP's children from the socket alone. It rides every frame of the child: `OrderPlaced`, taker fills and remainder cancels, maker fills, `OrderCancelled`, `OrderPartiallyCancelled`, `OrderExpired` and its `orders` snapshot row. It is omitted for an order that is not a TWAP child, and by servers that predate the field. Which frames each kind of child sends is described under [TWAP Orders Channel](#twap-orders-channel).
A margin order reports `positionId?: string` (wire `position_id`), the same id `getOrder` returns as `positionId`. An order placed while its margin account (cross) or risk bucket (isolated) already holds an open position in the pair is bound to that position when it is admitted, so a resting close or reduce-only order names its position before it ever fills. An order into a flat account or bucket is unbound until its first fill binds it, and that fill's frames carry it. If the bound position closes and a later fill opens a new one, the frames follow the new binding. It rides `OrderPlaced`, taker fills and remainder cancels, maker fills (the maker's own binding), `OrderCancelled`, `OrderPartiallyCancelled`, and `OrderExpired`. Those live events omit it while the order is unbound, always for spot, and on servers that predate the field. The `orders` snapshot differs: its `positionId: string | null` key is always present and is `null` for an unbound or spot order. A margin order's `OrderPlaced` also carries `marginAccountId`, `leverage` and `reduceOnly` as `getOrder` reports them; `reduceOnly` is present on every margin order, `false` included, and all three are omitted for spot.
A margin order's events also carry `marginAccountId?: string` (wire `margin_account_id`) on `CommonOrderEventData`: the margin account the order trades on — the default one or a sub-account — so an owner of several accounts can route events per account from the socket alone. It rides `OrderPlaced`, fills in both roles, remainder cancels and `OrderPartiallyCancelled`; a maker fill carries the **maker's** account, not the taker's. It is omitted for spot and on servers that predate the field.
An order admitted post-only likewise reports `postOnly?: boolean` (wire `post_only`) on its `OrderPlaced` acknowledgement, its fills in **both roles**, `OrderPartiallyCancelled`, `OrderCancelled` and `OrderExpired` — so a market maker driving state off this channel can confirm the flag was honoured without a round trip to `getOrder`, and keeps it across an in-place reduction rather than losing it mid-lifecycle. The maker fill matters most: an admitted post-only order can never produce a taker fill (a crossing one is refused at admission and emits nothing), so every fill a post-only order ever has is a maker fill — which now carries the flag. Two frames never carry it, and neither is reachable by a post-only order: `OrderRejected`, which only describes a MARKET order whose apparent liquidity is invalidated by maker-risk revalidation after acceptance, or an IOC/FOK unfilled for want of liquidity (one stopped by self-trade prevention is cancelled, not rejected — and a plain MARKET submit with no liquidity is refused before acceptance and emits nothing at all); and the `OrderCancelled` that reports the **remainder of an IOC or market order** after a partial fill, which is a different payload from the resting-order cancellation named above and declares no `postOnly` at all. Post-only is refused on MARKET and IOC/FOK alike, so a post-only order reaches neither. The key is only ever present as `true`; an order that is not post-only omits it rather than sending `postOnly: false`, exactly as the REST read model does, so `data.postOnly ?? false` reads the same as REST on every carrying variant.
Absent means "not post-only" — or an order placed before the flag was persisted, and a maker fill emitted by a server predating the maker-fill support — never "the flag was lost". For those pre-field orders, the order-level value learned from the `OrderPlaced` acknowledgement (or a REST read) remains the authority; for everything since, `?? false` is safe on any carrying variant, whichever event arrived last.
The structural snippets in this section assume these type imports:
```typescript theme={null}
import type {
OrderRole,
OrderSide,
OrderStatus,
OrderTerminalReason,
OrderType,
SelfTradePreventionMode,
TimeInForce,
TradingMode,
} from "@0xmonaco/types";
```
```typescript theme={null}
{
orderId: string;
eventType: "OrderPlaced";
timestamp: string;
data: {
orderId: string;
version?: number; // opaque same-order revision; absent means unknown
userId: string;
applicationId?: string;
tradingPairId: string;
tradingPair: string; // wire `trading_pair` — NOT `symbol`
tradingMode: TradingMode | (string & {});
orderType: OrderType | (string & {});
side: OrderSide | (string & {});
price?: string;
quantity: string;
timeInForce?: TimeInForce | (string & {}) | null; // null on a MARKET order's events and on a maker fill
status: OrderStatus | (string & {});
clientOrderId?: string; // correlation handle echoed on every order event (wire `client_order_id`)
postOnly?: boolean; // `true` when admitted post-only (wire `post_only`); omitted, never `false`, otherwise
marginAccountId?: string; // margin orders only (wire `margin_account_id`)
leverage?: string; // margin orders placed with a leverage (wire `leverage`)
reduceOnly?: boolean; // every margin order, `false` included (wire `reduce_only`); omitted for spot
positionId?: string; // the position the order is bound to at admission (wire `position_id`); omitted while unbound
// Only on the replacement of a partly filled order (an edit): the fill
// it carried over, so `quantity` is the order's total and `status` is
// PARTIALLY_FILLED, as REST reports it. Omitted on any other OrderPlaced.
filledQuantity?: string; // wire `filled_quantity`
filledQuantityRaw?: string; // RAW precision (wire `filled_quantity_raw`)
averageFillPrice?: string; // the carried fill's average (wire `average_fill_price`)
}
}
```
**When:** Order validated, persisted, funds locked - ready for matching.
**Recipient:** Taker only.
```typescript theme={null}
{
orderId: string;
eventType: "OrderMatched";
timestamp: string;
data: {
orderId: string;
version?: number; // opaque same-order revision; absent means unknown
userId: string;
tradingPairId: string;
tradingPair: string; // wire `trading_pair` — NOT `symbol`
tradingMode: TradingMode | (string & {});
// Same taker payload as OrderFilled: the engine builds one struct and
// picks the event type from the resulting status. Because this event
// carries only that one shape, the keys below are exact — a key is
// either always sent, or always sent carrying `null`. Of these execution
// fields, only the two last-fill keys are ever absent.
role: OrderRole | (string & {}); // emitted as "taker" on OrderMatched
tradesCount: number;
filledQuantity: string; // cumulative order fill (wire `filled_quantity`)
filledQuantityRaw: string; // RAW (integer, base-unit) precision
remainingQuantity: string;
remainingQuantityRaw: string; // RAW precision
fee: string; // total fee, normalized decimal QUOTE units: monacoFee + builderFee
feeRaw: string; // `fee` in RAW quote-unit precision (wire `fee_raw`)
monacoFee?: string; // Monaco's component of `fee` (wire `monaco_fee`)
monacoFeeRaw?: string; // `monacoFee` in RAW quote-unit precision (wire `monaco_fee_raw`)
builderFee?: string; // the builder fee component of `fee`; "0" for a maker (wire `builder_fee`)
builderFeeRaw?: string; // `builderFee` in RAW quote-unit precision (wire `builder_fee_raw`)
// Widened, like every value field the parser hands through: an
// unrecognized status is delivered rather than dropping the frame, so
// match it with a `default` branch.
status: OrderStatus | (string & {});
// Present but `null` when the event covers no trade — branch on null,
// do not test for the key being absent.
tradeId: string | null;
executionPrice: string | null;
executedAt: string | null; // RFC3339 with `Z`
averageFillPrice: string | null; // the order's average across every fill, an edit's carried fills included
// Slippage info. Present, `null` when unset.
referencePrice: string | null;
bestPrice: string | null;
worstPrice: string | null;
actualSlippageBps: number | null;
maxSlippageBps: number | null;
// Optional incremental-fill keys: omitted when this event filled
// nothing incrementally.
lastFillQuantity?: string; // this event's increment (wire `last_fill_quantity`)
lastFillQuantityRaw?: string; // RAW (integer, base-unit) precision
}
}
```
**When:** Order matched with counterparty but not yet filled.
**Recipient:** Taker only.
```typescript theme={null}
{
orderId: string;
eventType: "OrderFilled";
timestamp: string;
data: {
orderId: string;
version?: number; // opaque same-order revision; absent means unknown
userId: string;
tradingPairId: string;
tradingPair: string; // wire `trading_pair` — NOT `symbol`
tradingMode: TradingMode | (string & {});
role: OrderRole | (string & {});
status: OrderStatus | (string & {});
// Execution details
tradeId: string;
executionPrice: string;
filledQuantity: string; // cumulative order fill (wire `filled_quantity`) — same on every `role`
filledQuantityRaw?: string; // RAW (integer, base-unit) precision
lastFillQuantity?: string; // incremental fill from THIS event (wire `last_fill_quantity`); omitted when no fill
lastFillQuantityRaw?: string; // RAW precision
remainingQuantity: string; // Should be "0"
remainingQuantityRaw?: string; // RAW precision
postOnly?: boolean; // both roles, and only when admitted post-only (wire `post_only`); omitted, never `false`
fee: string; // Total fee for the fills THIS event covers, in decimal quote units; negative = maker rebate. monacoFee + builderFee
feeRaw?: string; // `fee` in RAW quote-unit precision, scaled by 10^quoteDecimals (wire `fee_raw`)
monacoFee?: string; // Monaco's component of `fee` (wire `monaco_fee`)
monacoFeeRaw?: string; // `monacoFee` in RAW quote-unit precision (wire `monaco_fee_raw`)
builderFee?: string; // The builder fee component of `fee`; "0" for a maker (wire `builder_fee`)
builderFeeRaw?: string; // `builderFee` in RAW quote-unit precision (wire `builder_fee_raw`)
executedAt: string; // RFC3339 with `Z`, e.g. "2026-01-02T03:04:05Z"
// Aggregate fields
tradesCount: number;
averageFillPrice: string; // the order's average across every fill (both roles), as REST reports it; this trade's price is `executionPrice`
// Slippage (taker only, null for maker)
referencePrice?: string;
bestPrice?: string;
worstPrice?: string;
actualSlippageBps?: number;
maxSlippageBps?: number;
}
}
```
**When:** Order completely filled.
**Recipient:** Both taker and maker.
```typescript theme={null}
{
orderId: string;
eventType: "OrderPartiallyFilled";
timestamp: string;
data: {
orderId: string;
version?: number; // opaque same-order revision; absent means unknown
userId: string;
tradingPairId: string;
tradingPair: string; // wire `trading_pair` — NOT `symbol`
tradingMode: TradingMode | (string & {});
role: OrderRole | (string & {});
status: OrderStatus | (string & {});
// Execution details
tradeId: string;
executionPrice: string;
filledQuantity: string; // cumulative order fill (wire `filled_quantity`) — same on every `role`
filledQuantityRaw?: string; // RAW (integer, base-unit) precision
lastFillQuantity?: string; // incremental fill from THIS event (wire `last_fill_quantity`); omitted when no fill
lastFillQuantityRaw?: string; // RAW precision
remainingQuantity: string;
remainingQuantityRaw?: string; // RAW precision
postOnly?: boolean; // both roles, and only when admitted post-only (wire `post_only`); omitted, never `false`
fee: string; // Total fee for the fills THIS event covers, in decimal quote units; negative = maker rebate. monacoFee + builderFee
feeRaw?: string; // `fee` in RAW quote-unit precision, scaled by 10^quoteDecimals (wire `fee_raw`)
monacoFee?: string; // Monaco's component of `fee` (wire `monaco_fee`)
monacoFeeRaw?: string; // `monacoFee` in RAW quote-unit precision (wire `monaco_fee_raw`)
builderFee?: string; // The builder fee component of `fee`; "0" for a maker (wire `builder_fee`)
builderFeeRaw?: string; // `builderFee` in RAW quote-unit precision (wire `builder_fee_raw`)
executedAt: string; // RFC3339 with `Z`, e.g. "2026-01-02T03:04:05Z"
// Aggregate fields
tradesCount: number;
// NOTE: no `totalFilled` on a fill payload — the cumulative fill is
// `filledQuantity` and this event's increment is `lastFillQuantity`
// `totalFilled` appears only on OrderCancelled and
// OrderRejected.
averageFillPrice?: string; // the order's average across every fill (both roles), as REST reports it; this trade's price is `executionPrice`
// Slippage (taker only, null for maker)
referencePrice?: string;
bestPrice?: string;
worstPrice?: string;
actualSlippageBps?: number;
maxSlippageBps?: number;
}
}
```
**When:** Order partially filled (more quantity remaining).
**Recipient:** Both taker and maker.
```typescript theme={null}
{
orderId: string;
eventType: "OrderCancelled";
timestamp: string;
data: {
orderId: string;
version?: number; // opaque same-order revision; absent means unknown
userId: string;
tradingPairId: string;
tradingPair: string; // wire `trading_pair` — NOT `symbol`
tradingMode: TradingMode | (string & {});
reason: string;
filledQuantity?: string; // cumulative order fill at cancellation (wire `filled_quantity`)
filledQuantityRaw?: string; // RAW (integer, base-unit) precision
totalFilled?: string; // legacy alias of `filledQuantity`
totalFilledRaw?: string; // RAW (integer, base-unit) precision
remainingQuantity?: string;
remainingQuantityRaw?: string; // RAW precision
tradesCount?: number;
averageFillPrice?: string; // VWAP of the cancelled order (wire `average_fill_price`)
cancelledAt?: string; // cancellation timestamp, RFC3339 with `Z` (wire `cancelled_at`)
postOnly?: boolean; // `true` when the cancelled order was resting post-only (wire `post_only`); omitted, never `false`
terminalReason?: OrderTerminalReason | (string & {}); // stable machine-readable cause (wire `terminal_reason`)
selfTradePreventionMode?: SelfTradePreventionMode | (string & {}); // the mode REQUESTED on this order (wire `self_trade_prevention_mode`); absent when it carried none
parentOrderId?: string; // the TWAP parent of a child order (wire `parent_order_id`); omitted for any other order
stpCounterpartyOrderId?: string; // self-trade-prevention counterparty (wire `stp_counterparty_order_id`); present ONLY with terminalReason `SELF_TRADE_PREVENTION`
}
}
```
**When:** Order cancelled by user, an `IOC` order's remainder cancelled after a partial fill, a market, `IOC` or `FOK` remainder (or a margin limit remainder) the price band cut short (`terminalReason` `PRICE_BAND`, or `SLIPPAGE_TOLERANCE` when the order's own `slippageToleranceBps` was tighter), a cancellation caused by [self-trade prevention](/developers/trade#self-trade-prevention), or a resting order the engine cancelled because its risk bucket entered liquidation or because auto-deleveraging closed the position a reduce-only order was written for. An `FOK` that cannot fill completely for want of liquidity is **rejected** instead — its candidate fills are discarded and it emits `OrderRejected` — but an `FOK` that meets one of your own orders under `CANCEL_TAKER` or `SKIP` is **cancelled**, arriving here as a zero-fill `OrderCancelled` rather than a rejection. (`CANCEL_BOTH` is the one that still rejects at placement.)
**Recipient:** Order owner.
**Reasons:**
* `"IOC order: remaining quantity cancelled after partial fill"`
* `"Market order: remaining quantity cancelled due to slippage limit"`
* `self_trade_prevention` — paired with `terminalReason` `SELF_TRADE_PREVENTION`
* `LIQUIDATION` — paired with `terminalReason` `LIQUIDATION`: the order's risk bucket entered liquidation, which cancels every resting order it holds
* `AUTO_DELEVERAGING` — paired with `terminalReason` `AUTO_DELEVERAGING`: auto-deleveraging closed the position this reduce-only order was written for
* A price-band cut carries `terminalReason` `PRICE_BAND` (the exchange's protective band stopped the order) or `SLIPPAGE_TOLERANCE` (the order's own `slippageToleranceBps` did); match on `terminalReason`, not the free-form `reason`
**Self-trade prevention.** `stpCounterpartyOrderId` and `terminalReason: "SELF_TRADE_PREVENTION"` always travel together, so the presence of the key is the whole signal — absent means this was not an STP 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. An STP cancellation is a real terminal `CANCELLED` even with zero fills, unlike an accepted zero-fill `IOC`/`FOK`, which is `REJECTED`. A plain zero-fill `MARKET` submit is refused before acceptance and emits neither frame.
```typescript theme={null}
{
orderId: string;
eventType: "OrderPartiallyCancelled";
timestamp: string;
data: {
orderId: string;
version?: number; // opaque same-order revision; absent means unknown
userId: string;
tradingPairId: string;
tradingPair: string; // wire `trading_pair` — NOT `symbol`
tradingMode: TradingMode | (string & {});
quantity: string; // reduced TOTAL: quantity = filledQuantity + remainingQuantity
status: OrderStatus | (string & {}); // non-terminal — the order is STILL RESTING
filledQuantity: string; // cumulative fill, unchanged by the reduction
filledQuantityRaw: string; // RAW (integer, base-unit) precision
remainingQuantity: string; // remaining after the reduction (the requested target)
remainingQuantityRaw: string; // RAW precision
reason: string | null; // cancellation reason; null when none (the SDK cancelOrder request takes no reason input)
reducedAt: string; // reduction timestamp, RFC3339 with `Z` (wire `reduced_at`)
clientOrderId?: string | null;
postOnly?: boolean; // `true` when the reduced order is resting post-only (wire `post_only`); omitted, never `false`
stpCounterpartyOrderId?: string; // declared for a future reducing mode; NOTHING emits it on this event today
}
}
```
**When:** A `cancelOrder(orderId, { remainingQuantityTarget })` partial cancel reduces a resting order in place. Self-trade prevention cancels a resting order **in full**, so it never produces this event — `stpCounterpartyOrderId` is declared here for a future reducing mode and is absent on every frame today. **Not a terminal event** — the order keeps its queue position and still rests; a later fill or `OrderCancelled` follows eventually.
**Recipient:** Order owner.
```typescript theme={null}
{
orderId: string;
eventType: "OrderRejected";
timestamp: string;
data: {
orderId: string;
version?: number; // opaque same-order revision; absent means unknown
userId: string;
tradingPairId: string;
tradingPair: string; // wire `trading_pair` — NOT `symbol`
tradingMode: TradingMode | (string & {});
reason: string;
status: OrderStatus | (string & {});
totalFilled: string;
totalFilledRaw: string; // RAW (integer, base-unit) precision
remainingQuantity: string;
remainingQuantityRaw: string; // RAW (integer, base-unit) precision
}
}
```
**When:** An order that was *accepted* is then rejected by the match — an `IOC`
that cannot fill at all, a `FOK` that cannot fill completely, or a `MARKET`
order whose apparent liquidity is invalidated by maker-risk revalidation after
acceptance. All of these are liquidity outcomes. An order stopped instead by
[self-trade prevention](/developers/trade#self-trade-prevention) is
`OrderCancelled` even with zero fills — the one exception being a `FOK` under
`CANCEL_BOTH`, which is refused at placement and emits nothing. Such a request returns `2xx` with `matchResult.status` `REJECTED`,
and emits `OrderPlaced` followed by `OrderRejected`, so a success response does
not mean it executed.
This event does **not** cover rejections raised *before* acceptance — a
validation failure, a post-only order that would cross, a `clientOrderId`
conflict, or a plain `MARKET` submit with no liquidity inside the protective
band, which is rejected at pricing before a sequence number is allocated. Those
fail the request itself and emit nothing.
**Recipient:** Order owner.
**Reasons:**
* `"IOC order rejected: no immediate fill available"`
* `"FOK order rejected: cannot be fully filled immediately"`
* `"FOK order rejected: partial fill not acceptable, must be fully filled"`
* `"Market order rejected: no liquidity available"` — reachable only when maker-risk
revalidation invalidates apparent liquidity after acceptance, not from a plain
market submit
```typescript theme={null}
{
orderId: string;
eventType: "OrderExpired";
timestamp: string;
data: {
orderId: string;
version?: number; // opaque same-order revision; absent means unknown
userId: string;
tradingPairId: string;
tradingPair: string; // wire `trading_pair` — NOT `symbol`
tradingMode: TradingMode | (string & {});
// Always sent, `null` rather than absent when there is no value —
// the emitter writes every key and maps an empty Option to null.
filledQuantity: string | null;
filledQuantityRaw: string | null; // RAW (integer, base-unit) precision
remainingQuantity: string | null;
remainingQuantityRaw: string | null; // RAW (integer, base-unit) precision
averageFillPrice: string | null;
reason: string;
postOnly?: boolean; // `true` when the expired order was resting post-only (wire `post_only`); omitted, never `false`
}
}
```
**When:** GTC order reaches expiration date (default 90 days).
**Recipient:** Order owner.
**Reasons:**
* `"GTC order expired after 90 days"`
* `"GTC order reached custom expiration date"`
### Edited Orders
Editing a partly filled order keeps its fills: the replacement (a new order id) carries them, and its frames report the order as `getOrder` does. Its `OrderPlaced` carries the order's total `quantity`, the carried fill in `filledQuantity`, that fill's `averageFillPrice`, and `status` `PARTIALLY_FILLED`. Its fill frames report the total `quantity`, the cumulative `filledQuantity` and the `averageFillPrice` across every fill, carried ones included, and a remainder `OrderCancelled` or an `OrderRejected` reports the same total and cumulative fill (these two payloads carry no average); `lastFillQuantity` (and `executionPrice` on a maker fill) cover only that event. The original order's `OrderCancelled` (`terminalReason` `REPLACED`) arrives first. Single and batch edits behave the same.
## Orderbook Channel
Subscribe to orderbook updates. No authentication required.
```typescript theme={null}
const pair = await sdk.market.getTradingPairBySymbol("BTC/USDC");
const unsubscribe = sdk.ws.orderbook(
pair.id, // Trading pair ID
"SPOT", // Trading mode
1, // Magnitude (price grouping)
"BASE", // Quotation mode: "BASE" or "QUOTE"
(event) => {
console.log("Best bid:", event.bestBid);
console.log("Best ask:", event.bestAsk);
console.log("Bids:", event.bids);
console.log("Asks:", event.asks);
}
);
// Cleanup
unsubscribe();
```
### Magnitude Parameter
The magnitude parameter groups orderbook levels by price intervals:
| Magnitude | Description |
| - | - |
| `0.0001` | Group by \$0.0001 |
| `0.001` | Group by \$0.001 |
| `0.01` | Group by \$0.01 |
| `0.1` | Group by \$0.1 |
| `1` | Group by \$1 |
| `10` | Group by \$10 |
| `100` | Group by \$100 |
| `1000` | Group by \$1,000 |
| `10000` | Group by \$10,000 |
## OHLCV Channel
Subscribe to real-time candlestick updates. No authentication required.
```typescript theme={null}
const pair = await sdk.market.getTradingPairBySymbol("BTC/USDC");
const unsubscribe = sdk.ws.ohlcv(
pair.id, // Trading pair ID
"SPOT", // Trading mode
"1m", // Interval: "1m", "5m", "15m", "1h", "4h", "1d"
(event) => {
const candle = event.candlestick;
console.log("Open:", candle.o);
console.log("High:", candle.h);
console.log("Low:", candle.l);
console.log("Close:", candle.c);
console.log("Volume:", candle.v);
}
);
// Cleanup
unsubscribe();
```
An optional fifth `priceType` argument (`"trade"` | `"mark"` | `"*"`) selects the price series. Omitting it — or passing `"trade"` — subscribes to the trade-derived candles exactly as above; `"mark"` subscribes to the close-only **mark-price** series for margin pairs (each candle's open = high = low = close = the minute's closing mark, volume `0`), and `"*"` receives both. The mark series is published on the **`1m` interval only** — a `"mark"` or `"*"` subscription at any other interval succeeds but receives no mark frames. The argument appends a fifth token to the `ohlcv` channel string, so existing four-token subscriptions are unchanged and never receive the mark series. When you pass `"*"`, read `event.priceType` (known series `"trade"` and `"mark"`, with unfamiliar strings passed through) on each frame to tell the two streams apart.
```typescript theme={null}
// Mark-price candles for a margin pair
const unsubMark = sdk.ws.ohlcv(pair.id, "MARGIN", "1m", (event) => {
console.log("mark close:", event.candlestick.c);
}, "mark");
```
An optional sixth `onSnapshot` argument receives the subscribed series' newest **350** candles, oldest first, once per subscription (and again after each reconnect) — the same candles as `sdk.market.getCandlesticks` with its default limit, including flat candles for periods with no trades. Each row is an `OHLCVSnapshotCandle`: the live `Candlestick` without `s`, with `i` set to the interval you subscribed to. Pass `undefined` for `priceType` to keep the trade series:
```typescript theme={null}
import type { OHLCVSnapshotCandle } from "@0xmonaco/types";
const candles = new Map();
const unsubscribe = sdk.ws.ohlcv(
pair.id,
"SPOT",
"1m",
(event) => {
// Upsert by period start: a live candle replaces the one with the same `T`.
candles.set(event.candlestick.T, event.candlestick);
},
undefined,
(snapshot) => {
for (const candle of snapshot) candles.set(candle.T, candle);
}
);
```
It is a recent-history baseline, not a gap-free replay. The snapshot reads recorded candles, which trail the live candles: the newest `1m` candle is the last closed minute and longer intervals can lag, so its newest candle can be partial or missing. A live candle is the full current state of its period, so upsert by `T`. A `"*"` price type gets no snapshot (one frame carries one series). An unknown pair, a `tradingMode` that is not the market's, and `"mark"` on a spot market get `onSnapshot([])`. `"mark"` snapshots are served on every interval, though live mark candles stream on `1m` only.
### OHLCV Event Structure
```typescript theme={null}
import type { Interval, TradingMode } from "@0xmonaco/types";
interface OHLCVEvent {
tradingPairId: string;
interval: Interval | (string & {});
tradingMode: TradingMode | (string & {});
priceType?: "trade" | "mark" | (string & {});
candlestick: Candlestick;
}
```
`priceType` identifies a concrete series, never the `"*"` subscription selector. Legacy frames may omit it. Keep a `default` branch for an unfamiliar series, interval, or trading mode.
### Candlestick Structure
```typescript theme={null}
interface Candlestick {
T: number; // Period start timestamp (ms, UTC)
t: number; // Period end timestamp (ms, UTC)
o: string; // Open price
h: string; // High price
l: string; // Low price
c: string; // Close price
v: string; // Volume
s: string; // Symbol
i: string; // Interval
n: number; // Number of trades
}
```
## Trades Channel
Subscribe to trade executions. No authentication required.
```typescript theme={null}
const pair = await sdk.market.getTradingPairBySymbol("BTC/USDC");
const unsubscribe = sdk.ws.trades(
pair.id, // Trading pair ID
(event) => {
console.log("Trade ID:", event.data.tradeId);
console.log("Price:", event.data.price);
console.log("Quantity:", event.data.quantity);
console.log("Maker side:", event.data.makerSide);
},
(trades) => {
// Optional: the market's newest 50 trades, newest first
console.log("Recent trades:", trades.length);
}
);
// Cleanup
unsubscribe();
```
The optional `onSnapshot` receives `TradeEventData[]`: the market's newest 50 trades, newest first, once per subscription (and again after each reconnect). It is a recent-history baseline, not a gap-free replay — a trade can arrive both in the snapshot and as an early live event, and one executed just before you subscribed can be in neither — so dedupe on `tradeId`. Older history comes from the REST trades read.
### Trade Event Structure
```typescript theme={null}
import type { OrderSide, TradingMode } from "@0xmonaco/types";
interface TradeEvent {
eventType: "trade";
tradingPairId: string;
tradingMode: TradingMode | (string & {});
data: {
tradeId: string;
price: string;
quantity: string;
makerSide: OrderSide | (string & {});
executedAt: string;
};
}
```
## Movements Channel
**New in v0.5.4** - Real-time balance movement tracking
Subscribe to real-time account balance movements. **Requires authentication.**
```typescript theme={null}
// Authenticate first
await sdk.login("your-client-id");
// Subscribe to movements
const unsubscribe = sdk.ws.movements((event) => {
console.log("Movement ID:", event.data.id);
// Live types: "deposit", "withdrawal", "funding" (lowercase) and the order
// unlocks "OrderCancelled", "OrderPartiallyCancelled", "OrderExpired", whose
// entryType is uppercase "CREDIT". Fills and fees never arrive here — read
// them from movement history, and take a fill's live signal off `balances`.
console.log("Transaction:", event.data.transactionType);
console.log("Entry:", event.data.entryType);
console.log("Token:", event.data.tokenAddress);
console.log("Amount:", event.data.amount);
console.log("Balance:", event.data.balanceBefore, "→", event.data.balanceAfter);
// Check if related to an order
if (event.data.referenceType === "order") {
console.log("Order ID:", event.data.referenceId);
}
// Check for on-chain transaction
if (event.data.txHash) {
console.log("TX Hash:", event.data.txHash);
}
});
// Cleanup
unsubscribe();
```
### Movement Event Structure
```typescript theme={null}
interface UserMovementEvent {
eventType: "movement";
userId: string;
data: {
id: string; // Movement ID (UUID)
entryType: string; // free-form; "credit" | "debit" | "CREDIT" (order unlocks) — compare case-insensitively
transactionType: string; // free-form; live: "deposit" | "withdrawal" | "funding" | "OrderCancelled" | "OrderPartiallyCancelled" | "OrderExpired". History adds fill/fee types this channel never emits
status?: string; // Movement status: "completed" | "pending" | "failed" (wire `status`)
token?: string; // Token contract address (wire `token`; older payloads use the `token_address` alias)
tokenAddress: string; // Populated legacy alias of `token` (always set, for back-compat)
assetId?: string; // Asset registry ID, UUID (wire `asset_id`)
symbol?: string; // Token symbol (e.g., "USDC")
decimals: number; // Token decimals (e.g., 6 for USDC)
amount: string; // Movement amount (decimal string, normalized)
amountRaw?: string; // Raw amount (integer string, no decimals)
balanceBefore?: string; // Balance before movement (normalized)
balanceBeforeRaw?: string; // Balance before in raw format (wei)
balanceAfter?: string; // Balance after movement (normalized)
balanceAfterRaw?: string; // Balance after in raw format (wei)
lockedBefore?: string; // Locked balance before (normalized)
lockedBeforeRaw?: string; // Locked balance before in raw format (wei)
lockedAfter?: string; // Locked balance after (normalized)
lockedAfterRaw?: string; // Locked balance after in raw format (wei)
referenceId?: string; // Related entity ID (e.g., order UUID)
referenceType?: string; // Reference type ("order", "deposit", etc.)
description?: string; // Human-readable description
txHash?: string; // On-chain transaction hash (if applicable)
blockNumber?: number; // Block number, if on-chain (wire `block_number`)
createdAt?: string; // ISO 8601 timestamp (UTC)
};
}
```
### Example: Track Deposits
```typescript theme={null}
sdk.ws.movements((event) => {
if (event.data.transactionType === "deposit" && event.data.txHash) {
console.log(`✅ Deposit confirmed: ${event.data.amount}`);
console.log(`New balance: ${event.data.balanceAfter}`);
console.log(`TX: ${event.data.txHash}`);
}
});
```
### Example: Monitor Order Unlocks
```typescript theme={null}
sdk.ws.movements((event) => {
// A cancelled, partially cancelled, or expired order releases its lock.
if (event.data.transactionType.toLowerCase().startsWith("order")) {
console.log(`Order ${event.data.referenceId}: ${event.data.amount} unlocked`);
}
});
```
A fill and its fee never arrive on this channel. Track a fill live through `sdk.ws.orders` (the lifecycle event) and `sdk.ws.balances` (the resulting balance — a versioned spot row for a spot leg, an unversioned margin-collateral frame for a perp fill), and read its ledger entries — including fee and rebate rows — from movement history with `sdk.profile.getPaginatedUserMovements()`.
### React Hook
For React applications, use the dedicated hook:
```tsx theme={null}
import { useUserMovements } from "@0xmonaco/react";
function MovementsFeed() {
const { movements, loading, subscribed } = useUserMovements({ maxMovements: 50 });
return (
{movements.map(m => (
{m.data.transactionType}: {m.data.amount}
))}
);
}
```
[Learn more about useUserMovements →](/sdk/react/hooks/use-user-movements)
## Conditional Orders Channel
Stream TP/SL conditional-order lifecycle events. **Requires authentication.** Authentication is checked at subscribe time — unauthenticated subscribe attempts are rejected. Ensure the SDK is authenticated before calling `sdk.ws.conditionalOrders(...)`.
### Subscribe to Conditional Orders
```typescript theme={null}
// All conditional-order events for the authenticated user
const unsubscribe = sdk.ws.conditionalOrders((event) => {
const { conditionalOrderId, state, reason, triggerPrice, triggeredOrderId } = event.data;
console.log(`${conditionalOrderId} → ${state} (${reason})`);
});
// Scope to a single trading pair (optional second arg)
const unsubscribePair = sdk.ws.conditionalOrders(
(event) => { /* ... */ },
tradingPairId,
);
// Cleanup
unsubscribe();
```
### Conditional Order Event Structure
```typescript theme={null}
import type {
ConditionalOrderConditionType,
ConditionalOrderState,
ConditionalOrderTriggerSource,
OrderSide,
OrderType,
PositionSide,
TimeInForce,
} from "@0xmonaco/types";
{
eventType: "conditional_order_update";
userId: string;
data: {
conditionalOrderId: string;
version?: number; // revision of this row, on every update; compare with >=
tradingPairId: string;
marginAccountId: string;
positionId?: string;
parentOrderId?: string;
associationType?: "POSITION" | "PARENT_ORDER" | (string & {});
linkedGroupId?: string; // populated for OCO TP/SL pairs
conditionType: ConditionalOrderConditionType | (string & {});
triggerSource: ConditionalOrderTriggerSource | (string & {});
triggerPrice: string;
side: OrderSide | (string & {});
positionSide: PositionSide | (string & {});
orderType: OrderType | (string & {});
limitPrice?: string;
quantity?: string;
slippageToleranceBps?: number;
reduceOnly: boolean;
timeInForce?: TimeInForce | (string & {}); // full enum, not just GTC/IOC — narrow before use
state: ConditionalOrderState | (string & {});
triggeredOrderId?: string; // the close the trigger placed; set from the "triggered" update on
triggeredAt?: string;
activatedAt?: string; // populated when parent-attached TP/SL becomes active
cancelledAt?: string;
expiresAt?: string;
failureReason?: string; // populated on FAILED and EXPIRED, and on a refused trailing stop
trailBps?: number; // TRAILING_STOP only: retracement behind the watermark, in bps
activationPrice?: string; // TRAILING_STOP only: level that starts tracking; absent when armed immediately
watermarkPrice?: string; // TRAILING_STOP only: best mark since arming; triggerPrice trails it
trailArmedAt?: string; // TRAILING_STOP only: when tracking began; absent while awaiting activationPrice
reason: string; // free-form; canonical values listed below
createdAt?: string; // same string REST returns; always sent by current servers, absent only from an older deployment
updatedAt: string;
}
}
```
Neither the live frames nor the subscribe-time snapshot carries `triggeredOrder`, the summary of what the triggered order has done — it is a REST-only field on `getConditionalOrder` and `listConditionalOrders`. `triggeredOrderId` is the id alone: the live `reason: "triggered"` update carries it with `state: "TRIGGERED"` and `triggeredAt`, the same values REST returns. The subscribe-time snapshot lists only the non-terminal states (`PENDING_PARENT`, `ACTIVE`, `TRIGGERING`), never a `TRIGGERED` row, so a trigger that fires while you are disconnected is not replayed on reconnect: read it from REST, along with the close's status, filled quantity and average fill price. The close's own reverse pointer, `Order.conditionalOrderId`, is REST-only on the same terms: no order event and no order snapshot row carries it either, so a close first seen over the socket acquires it on the next REST read. See [Trigger Price Is Not Fill Price](/developers/trade#trigger-price-is-not-fill-price).
Conditional-order `version` is on every live update, on the subscribe-time snapshot and on REST reads, always the revision of the row the update describes. It is opaque and compare-only, for the same conditional order, with absent meaning unknown rather than zero. When both sides carry one, merge with `incoming.version >= local.version`: `>=` because one step can write several conditional orders at one version (a trigger and its OCO sibling), so equal means "same step". Never compare an absent version numerically (`42 >= undefined` is false): a versioned row supersedes an unversioned one, and two unversioned rows compare on `updatedAt`. Do not compare versions across different orders.
### Value unions are what the server sends today, not a guarantee
`@0xmonaco/core` parses this frame **leniently**: an unrecognized `orderType`, `timeInForce`, `state`, `side`, `positionSide`, `conditionType`, `triggerSource`, `associationType`, or `reason` is passed through as-is rather than rejected. Throwing inside the message handler would drop the whole update with nothing surfaced to the consumer, and a dropped frame is the worse failure — the position, liquidation, and market-stats parsers already make the same trade. Only `eventType` stays strict, so a mis-routed frame is still rejected.
Match these fields with a `default` branch rather than relying on exhaustiveness:
* `reason` is **free-form** on the wire. Every transition sends exactly one update. The canonical live `reason` values are `created`, `activated` (a `PENDING_PARENT` order went `ACTIVE` when its entry order filled), `triggered`, `oco_cancelled` (its linked sibling triggered), `parent_cancelled` (its entry order was cancelled, expired, replaced away or swept before it filled), `position_closed` (its position closed), `cancelled` (a user cancel, a new full-close order superseding it, or a trailing stop refused because the position already has one, with the reason in `failureReason`), `reparented` (a `PENDING_PARENT` order moved onto its entry order's replacement: `parentOrderId` changes, `state` does not), `failed`, `expired`, plus `armed` and `ratcheted` on a `TRAILING_STOP` row (tracking began; the trigger moved, each `ratcheted` frame carrying the new `triggerPrice` and `watermarkPrice`). They are exported as `ConditionalOrderEventReason` for narrowing, and the subscribe-time snapshot reuses the same shape with `snapshot`. Any value the server adds arrives unchanged.
* `timeInForce` is the full `TimeInForce` enum, not the `GTC`/`IOC` subset the creation paths validate: it mirrors an unconstrained server column that maps exhaustively over every variant, so a value written by another path reaches the wire as-is.
* `orderType` is `LIMIT` or `MARKET` in practice — both creation paths reject anything else — but that rests on validation rather than an exhaustive mapping, so a relaxed validation would surface the raw variant (e.g. `STOPLOSS` rather than `STOP_LOSS`).
### Lifecycle and recipients
| State | Meaning |
| - | - |
| `PENDING_PARENT` | Attached to a parent order that has not filled yet |
| `ACTIVE` | Created, waiting for trigger |
| `TRIGGERING` | Trigger condition met, underlying order being submitted |
| `TRIGGERED` | Underlying order submitted (now follows the standard order lifecycle via `userOrders` / `orders`) |
| `CANCELLED` | Cancelled by the user, by its OCO sibling triggering, with its entry order, by its position closing, by a superseding full-close order, or refused as a second trailing stop (`reason` says which) |
| `EXPIRED` | The trigger fired but the position it was attached to was no longer open, had been replaced, or had flipped side (see `failureReason`) |
| `FAILED` | Trigger fired but underlying order submission failed (see `failureReason`) |
The triggered order itself emits regular `OrderEvent` messages on the `orders` channel. To follow a TP/SL all the way through to fill, subscribe with either `sdk.ws.orders` for its pair or `sdk.ws.userOrders` for all pairs.
For higher-level usage, see [Order Management](/developers/trade#conditional-perps-orders) and [`sdk.positions.attachPositionTpSl`](/sdk/typescript/positions).
## Positions Channel
Stream live position state — size, mark price, unrealized PnL, liquidation price, isolated margin, and leverage. **Requires authentication**; the signed session-key handshake is sent before the subscribe. Pass an optional trailing `tradingPairId` to filter to one pair; omit it to stream every pair.
The channel carries two kinds of frame: lifecycle `position_update` frames, delivered to the handler, and periodic [`position_valuation` frames](#position-valuation-frames), delivered only to the optional `onValuation` callback.
Detected lag closes the whole socket with 1013 under the [slow-client rule](#slow-client-disconnect-close-code-1013). Reconcile lifecycle frames on `version` per `positionId` (see the merge rule below), never on `updatedAt`, and refetch a snapshot on reconnect (see [Reconnect and resync](/developers/market-maker-runbook#4-reconnect-and-resync)).
### Subscribe to Positions
```typescript theme={null}
// All position updates for the authenticated user
const unsubscribe = sdk.ws.positions((event) => {
const { positionId, side, status, size, markPrice, unrealizedPnl, liquidationPrice } = event.data;
console.log(`${positionId} ${side} ${status} size=${size} mark=${markPrice} uPnL=${unrealizedPnl}`);
});
// Scope to a single trading pair (optional second arg)
const unsubscribePair = sdk.ws.positions((event) => { /* ... */ }, tradingPairId);
// Periodic valuations (optional fourth arg): apply them only to positions you hold as OPEN
const unsubscribeWithValuations = sdk.ws.positions(
(event) => { /* lifecycle frames */ },
undefined,
undefined,
(valuation) => {
const held = positions.get(valuation.data.positionId);
if (held?.status !== "OPEN") return; // unknown or terminal: ignore
// read before the held state's last mutation: stale, ignore
if (Date.parse(valuation.data.valuedAt) <= Date.parse(held.updatedAt)) return;
Object.assign(held, {
markPrice: valuation.data.markPrice,
unrealizedPnl: valuation.data.unrealizedPnl,
liquidationPrice: valuation.data.liquidationPrice ?? undefined,
initialMarginRequired: valuation.data.initialMarginRequired,
maintenanceMarginRequired: valuation.data.maintenanceMarginRequired,
});
},
);
// Cleanup
unsubscribe();
```
### Position Event Structure
```typescript theme={null}
import type { MarginMode, PositionEventSide, PositionEventStatus } from "@0xmonaco/types";
{
eventType: "position_update";
userId: string;
data: {
positionId: string;
marginAccountId: string;
tradingPairId: string;
side: PositionEventSide | (string & {});
status: PositionEventStatus | (string & {}); // SCREAMING_SNAKE_CASE, same casing as REST
size: string; // absolute size (decimal string)
entryPrice: string; // decimal string
markPrice?: string; // decimal string
unrealizedPnl?: string; // decimal string
liquidationPrice?: string; // decimal string
isolatedMargin: string; // the position's backing (decimal string) — the same value the REST position's isolatedMargin serves
leverage: string; // decimal string
riskBucketId?: string; // risk bucket scoping the position — the same value REST's riskBucketId serves
marginMode?: MarginMode | (string & {}); // "CROSS" | "ISOLATED"; never infer it from riskBucketId
maintenanceMarginRequired?: string; // required at markPrice (decimal string); "0" with no open exposure
initialMarginRequired?: string; // required at markPrice (decimal string); "0" with no open exposure
tradeId?: string; // trade that caused the update, present when a fill drove it
orderId?: string; // order that caused the update (falls back to the position's opening order)
reason: string; // "trade" | "close" | "liquidation" | "adl" | "funding" | "margin_added" | "margin_reduced"
updatedAt: string; // ISO 8601 (UTC) — when the position was last MUTATED, not when the frame was built
openedAt?: string; // ISO 8601 (UTC) — when the position OPENED; never moves; absent when unknown
version?: number; // opaque durable row revision; absent means unranked
}
}
```
A `position_update` frame now carries a position's **scope and margin requirements**, so the common case — knowing which risk bucket a position sits in, whether it is cross or isolated, and what it requires at the current mark — no longer needs a REST read. `positionId`, `marginAccountId`, `tradingPairId`, `riskBucketId`, `marginMode`, `status`, `side`, `size`, `entryPrice` and `isolatedMargin` are the same values on the subscribe-time snapshot, on the live frames, and on `GET /api/v1/positions`.
The frame is still not a superset of the REST position. `indexPrice`, `realizedPnl`, `netRealizedPnl`, `exitPrice`, `fundingPaid`, `feesPaid` and `cumFees` are REST-only, so a consumer that needs any of those still reads REST.
The six re-derived values (`markPrice`, `unrealizedPnl`, `liquidationPrice`, `leverage`, `maintenanceMarginRequired`, `initialMarginRequired`) are recomputed per payload rather than read from the versioned row, so take them from the newest frame, as the merge rule below already requires. Five of them are priced at whichever mark the answering surface holds, so a snapshot row and a live frame can differ by one oracle tick — the snapshot is a database baseline and prices its whole row from a single mark, which keeps each row internally consistent. `leverage` is the exception: it is opening notional over posted margin, which no reprice touches, and it is in this group because the live surfaces derive it from engine state while the persisted row serves its stored `leverage` column, so the two can disagree at an unchanged version.
A position with no risk bucket — one opened before risk buckets existed — reports neither `riskBucketId` nor `marginMode`. Do **not** infer either field from the other: they are resolved from different sources — the id from the position, the mode from the matching engine when it holds the position and from the risk-bucket record otherwise — so an id can arrive with no mode, on a terminal row whose record is unavailable, on a read taken while the engine is unreachable, or from a producer older than the field. 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.
`updatedAt` is when the position was last **mutated** — a fill, collateral transfer, funding settlement or close — carried from the producer's own record of that mutation. Two frames for a position nothing has changed repeat one timestamp, and a valuation-only oracle reprice does not move it. It is display metadata: reconcile on `version`, never on `updatedAt`.
`openedAt` is when the position **opened** — the fill that took its exposure from zero to non-zero. It rides the snapshot rows and the live frames, terminal frames included, and never moves for the life of `positionId`: time held is `now - openedAt` while open and `updatedAt - openedAt` on the terminal frame. A flip closes the position and opens a new one with a new `positionId` and its own `openedAt`. It is absent when the server does not know it — a terminal frame whose close it could not attach the instant to, or a frame from an older server — and is never filled with the time the frame was sent; keep the value you already hold for that `positionId`. Clamp a computed time held at zero: the value can sit slightly ahead of your clock, and after a server restart it can move by the server's own clock drift. It is not versioned: take it from the newest payload — frame or REST read — rather than skipping it on an equal `version`, because a one-off server correction can move it earlier without a version change.
The match across surfaces is exact for an **open** position. A **terminal** REST read carries the instant the matching engine emitted the close, which can differ slightly from the live frame's close instant; older terminal rows may instead carry the persistence clock and trail the close by the persistence lag. Any position read after a matching-engine restart is also served from the persistence clock and differs by that lag. Treat it as accurate to that lag.
One further case is not bounded by that lag. While the matching engine is unreachable, a REST read of an open position falls back to that row's lifecycle timestamps — close, then last funding, then open — so a position filled repeatedly since it opened reports its opening instant until funding settles. A live `position_update` frame is unaffected, being built from engine state, so a REST value older than a frame you already hold is expected in that window and the frame is the fresher one.
`version` is the position's opaque, non-negative durable row revision. The subscribe-time snapshot and a **ranked** live frame for identical position state carry the same value, so a client reconciles on `positionId + version`. Never use `updatedAt`, arrival order, or receiver backlog to establish ordering. Not every live frame is ranked — see the note below for which are, and treat an unranked frame as unranked rather than assuming it is comparable with a snapshot row.
The merge rule is **per field group**, not per frame — a frame is never discarded wholesale:
| Incoming version vs. held | Versioned fields (`size`, `entryPrice`, `status`, `marginMode`, ...) | `isolatedMargin` | Valuation fields (`markPrice`, `unrealizedPnl`, `liquidationPrice`, `leverage`, `maintenanceMarginRequired`, `initialMarginRequired`) |
| - | - | - | - |
| Higher | apply | apply | apply |
| Equal | skip — an idempotent redelivery | **apply** — see below | **apply** — they carry no version; five of them move on every oracle reprice, and `leverage` is derived per payload rather than served from the row |
| Lower | skip | skip | **skip** — a ranked frame can still arrive late, and taking its valuation fields would regress the displayed mark and PnL |
| Either side unranked | compare the versioned fields directly | apply the later-arriving payload | apply the later-arriving payload |
`isolatedMargin` gets its own column because it is a versioned field that the subscribe-time snapshot does **not** read from the versioned row: the snapshot overlays it from an independent live matching-engine read, so it can reflect a later command than the row's version names. Take it from the newest payload rather than skipping it on an equal version, or you can retain stale backing. Sourcing it atomically with the row is tracked separately.
Note the asymmetry between the Equal and Lower rows. An equal version means the *same* command, so its valuation fields are a legitimately newer sample of the same position state. A lower version means an *older* command, so nothing it carries is newer — including its mark. Arrival order is only a fallback where there is no version to compare at all; it must never override a positive version, for the same reason the first paragraph rules it out as an ordering signal.
Discarding a whole frame on an equal version is the one mistake this field is likely to cause: it freezes displayed unrealized PnL until the position's next real mutation, which for an idle position is the next hourly funding settlement.
Only a POSITIVE version is rankable, and "unranked" has two wire spellings that mean exactly the same thing:
* On the **WebSocket** — live frames and the subscribe-time snapshot alike — the key is **absent**. The snapshot maps the persisted `0` sentinel to an absent key before it reaches the wire, so a `positions` payload never carries a literal `0`.
* On **REST/gRPC**, where the field is a required int64 and cannot be omitted, it is the literal `0`.
Normalize the two together and fall back to a full field comparison rather than ranking either as the oldest revision.
**An absent version never tells you whether anything was persisted.** Current unranked frames cover two situations:
* The emit path wrote no position row at that sequence, so there is no versioned row to point at.
* The frame came from a producer that predates position versions.
Funding and terminal frames are normally ranked: funding carries exact post-settlement state captured inside its sequencer command, and zero payments emit no frame; terminal frames retain lifetime reduced quantity (the sum of every reducing execution), lifetime allocated initial margin, and the exact `CLOSED`/`LIQUIDATED` classification. A terminal frame remains unranked after a restart when legacy reduction history lacks size or margin fields needed to calculate complete lifetime values. Incomplete size history carries only the known partial total. This is an explicit compatibility fallback, not version `0`.
The version advances on sequenced mutations: open, add, reduce, partial and full close, funding, margin transfer, liquidation and ADL — exactly when the row's versioned content changed at that sequence, including a change that landed and was undone within one persistence batch. A sequenced command carries its whole margin account's open positions, but a position the command left unchanged keeps its version (and its `updatedAt`): an untouched sibling of a filled position does not move. A **ranked** live frame is stamped with its command's sequence, so a frame for such a sibling can carry a higher version than the REST row while describing identical content — rank it as newer and apply it; nothing changes. Advancing still never misses a change: every mutation of the row's versioned fields stamps a new version.
`maintenanceMarginRequired` and `initialMarginRequired` sit in the valuation column for the same reason `markPrice` does: both are computed from the current mark, so they move on an oracle tick with no mutation behind them.
A **valuation-only** oracle reprice — the overwhelming majority — persists nothing and advances no version, which is the whole point of the carve-out; the new figures reach clients through [`position_valuation` frames](#position-valuation-frames) instead. A **state-transitioning** oracle command (a fresh maintenance-margin breach, or a recovery out of one) does persist the affected account's snapshot, so any open position whose versioned content it changed is restamped like any other sequenced mutation. Both cases are safe to merge with the rules above; the distinction matters only if you are reasoning about how often the version moves.
**Which live frames are ranked.** A frame carries a version only when it is a faithful projection of the row persisted at that sequence. That includes fills, non-zero funding settlements, terminal closes/liquidations/ADL with complete lifetime margin history, an isolated position's direct margin add or reduction, and a copy-position margin add. Each is built from captured post-command state. A copy-position margin add refreshes both the isolated child and every open position in its copy cross risk bucket at the same version.
These frames are deliberately **unranked**:
* Frames whose command did not write position rows at that sequence. A parent-to-cross-risk-bucket `ALLOCATE` or `RELEASE` refreshes every open position in that risk bucket; copy follow creation or allocation growth and copy stop return refresh the copy cross risk bucket. These carry `margin_added` or `margin_reduced` but no version.
* A terminal frame after restart when pre-upgrade reduction history has an incomplete close payload. The frame still reports the terminal status and known values, but omits `version` so partial lifetime size or margin cannot compete with REST's fallback at an equal version. Reconcile it by full field comparison; never coerce the missing version to `0`.
Bucket-level transfer frames are unranked because the selected position rows did not advance at that sequence.
Wallet-to-margin `IN`/`OUT`, a plain isolated-risk-bucket `ALLOCATE` with no position collateral delta, and any selected risk bucket with no open positions send no position frame. Transfer frames are built only after the command succeeds and are released only after its durable acknowledgement.
### Position Valuation Frames
A mark move changes an open position's mark price, unrealized PnL, liquidation price and margin requirements without any lifecycle event behind it. `position_valuation` frames refresh those five figures for every open position about every five seconds, on the same `positions` and `positions:{tradingPairId}` channels, to the same owner. They are rolling out per environment; where they are not yet enabled the channel carries lifecycle frames only.
```typescript theme={null}
import type { PositionValuationEvent } from "@0xmonaco/types";
{
eventType: "position_valuation";
userId: string;
data: {
positionId: string;
marginAccountId: string;
tradingPairId: string;
markPrice: string; // decimal string
unrealizedPnl: string; // decimal string
liquidationPrice: string | null; // null when no liquidation price can be derived
initialMarginRequired: string; // required at markPrice (decimal string)
maintenanceMarginRequired: string; // required at markPrice (decimal string)
valuedAt: string; // ISO 8601 (UTC) — when the server read the position
}
}
```
* **Valuation only.** A valuation carries no `size`, `side`, `status`, `reason`, `updatedAt` or `version`, and never opens, closes, resizes or reopens a position. Lifecycle stays on `position_update`.
* **Apply it only to a position you hold as `OPEN`, and only when `valuedAt` is later than that position's `updatedAt`.** Valuations are spread over each interval, so one can arrive after a newer `position_update` for the same position — a fill, a close or a funding settlement that happened after the server read it. `valuedAt` and `updatedAt` come from the same server clock, so a valuation whose `valuedAt` is not later than the `updatedAt` you hold predates that state: ignore it, and the next valuation brings current figures. Ignore a valuation for an unknown or terminal `positionId` too. The server also withholds a valuation read before a `position_update` it has already sent for that position, including updates such as a risk bucket transfer that do not move `updatedAt`, so the rule is a backstop. Under these rules a late valuation can neither resurrect, close nor roll back a position.
* **Same figures as a lifecycle frame.** The five figures come from the same derivation as a `position_update` frame for that position at the same state; apply them like that frame's valuation fields. They carry no version, so they never rank against a lifecycle frame's versioned fields.
* **Cadence.** A position's frame is sent the first time it is read, then only when one of the five figures changed, and otherwise periodically, normally at least once a minute. A position closed in between gets no final valuation: its terminal `position_update` is the last word.
* **`liquidationPrice` is `null`** when none can be derived, for example an isolated risk bucket shared by several positions or one whose collateral basis is not positive. Keep the key; it is always present.
* **`valuedAt`** is when the server read the position. It is not the oracle's publish time and not `updatedAt`, which moves only on a mutation.
`handler` never receives a valuation, and without `onValuation` the SDK ignores them silently. SDK versions released before `onValuation` log one parse error per valuation frame and keep the socket open, so upgrade before relying on the channel staying quiet.
A position closed or liquidated **by an order fill** emits a terminal event whose retained `size`, lifetime `isolatedMargin`, status and—when history is complete—version match the persisted terminal row. `size` is lifetime reduced quantity: a position opened at `10`, reduced by `4`, then closed by `6` reports `10`, not the exposure before the final close. Detect closure from `status`, never from `size === "0"`. Positions closed by the **liquidation worker or auto-deleveraging** do the same: liquidation victims, backstop exits and the bankrupt ADL side report `LIQUIDATED`; an ADL counterparty flattened while absorbing the deleverage reports `CLOSED`; `reason` remains `"liquidation"` or `"adl"`. A surviving or partially reduced counterparty receives an `OPEN` refresh. A non-zero **funding settlement** emits a ranked `position_update` with `reason` `"funding"` carrying the command's captured post-settlement state; zero payments emit no position frame.
**Which positions a fill's frames cover.** A fill sends a live `OPEN` frame for every open position in each risk bucket it touched, on both sides of the trade: the taker's bucket, through the position its order is bound to (the order's `positionId`), and each maker's bucket, through the position that maker's resting order is bound to. A **cross** risk bucket's positions share its collateral, so a trade in one pair moves every other position's `liquidationPrice` in that bucket, and each of them, in any pair, gets a frame. An **isolated** risk bucket holds a single position, so it gets that one frame. Positions in the account's other risk buckets are not re-sent, including a position in the same pair held in a different bucket (a copy-trading bucket, for example). A position is sent at most once per fill even when it is on both sides, and every frame carries the fill's `version`. A position whose content the fill did not change can therefore arrive with a higher version than its REST row, which the version rule above already covers. Each frame names its own `tradingPairId`, so a `positions:{tradingPairId}` subscription receives the frames for positions in its pair even when the trade was in another pair. Liquidation slices and auto-deleveraging follow the same rule for the liquidated account's risk bucket and each counterparty's risk bucket. A resting placement that trades nothing sends at most one frame, for the position the order is bound to.
## Liquidations Channel
Stream liquidation alerts — the authenticated account crossed a maintenance threshold and a liquidation record was opened or advanced. **Requires authentication**; the signed session-key handshake is sent before the subscribe. Pass an optional trailing `tradingPairId` to filter to one pair; omit it to receive account-scoped alerts (which arrive on the bare `liquidations` channel) as well as pair-scoped ones.
Lag policy is **Disconnect**: a subscriber that falls behind has its **whole** connection closed with `1013` (try again later), taking every other subscription on that socket down with it — see [Slow-client disconnect](#slow-client-disconnect-close-code-1013). There is no wire sequence number, so treat a `1013` close as the gap signal and resync by refetching a snapshot — see [Reconnect and resync](/developers/market-maker-runbook#4-reconnect-and-resync).
### Subscribe to Liquidations
```typescript theme={null}
// All liquidation alerts for the authenticated user
const unsubscribe = sdk.ws.liquidations((event) => {
const { liquidationId, status, riskBucketState, estimatedShortfall, message } = event.data;
console.warn(`liquidation ${liquidationId} ${status}/${riskBucketState} shortfall=${estimatedShortfall}: ${message}`);
});
// Scope to a single trading pair (optional second arg)
const unsubscribePair = sdk.ws.liquidations((event) => { /* ... */ }, tradingPairId);
// Cleanup
unsubscribe();
```
### Liquidation Event Structure
```typescript theme={null}
{
eventType: "liquidation_alert";
userId: string;
data: {
liquidationId: string;
marginAccountId: string;
positionId?: string; // present when the alert is position-scoped
tradingPairId?: string; // present when the alert is position-scoped
status: string; // liquidation record status (e.g. "UNRESOLVED", or "COMPLETED" on a terminal alert)
riskBucketState: string; // liquidated risk bucket's state (e.g. "PENDING_LIQUIDATION")
liquidationPrice?: string; // decimal string
markPrice?: string; // decimal string
estimatedShortfall: string; // decimal string
message: string; // human-readable alert message
triggeredAt: string; // ISO 8601 timestamp (UTC)
}
}
```
An episode ends with a terminal alert whichever way it resolves. When a breached risk bucket heals back over its maintenance requirement — on a liquidation step, or on an oracle mark, a funding settlement, or an auto-deleveraging close that lifts it — its record closes and you receive one alert with `status: "COMPLETED"`, `riskBucketState: "Normal"`, and the episode's `liquidationId`. The same `COMPLETED` / `Normal` pair also closes an episode that was liquidated to completion and left the account healthy, so the pair marks the episode's end, not a recovery — do not infer from it whether a liquidation executed. Let the terminal alert supersede any `UNRESOLVED` snapshot row for the same `liquidationId`. See [Recovered episodes](/reference/websockets#recovered-episodes).
## Copy Channel
Stream your [copy trading](/developers/copy-trading) follows as a **follower**: a `copy_relationship_update` frame on every change to one of your follows, and a `copy_skip` frame for every leader fill a follow could not mirror. **Requires authentication.** There is no per-pair variant, and a leader receives nothing on this channel. In React, use [`useCopyEvents`](/sdk/react/hooks/use-copy-trading#usecopyevents).
```typescript theme={null}
const unsubscribe = sdk.ws.copy(
(event) => {
switch (event.eventType) {
case "copy_relationship_update": {
const { relationshipId, status, change, allocation, consecutiveSkips, version } = event.data;
console.log(`follow ${relationshipId} ${status} (${change}) allocation=${allocation} skips=${consecutiveSkips} v=${version}`);
break;
}
case "copy_skip": {
const { relationshipId, tradingPairId, reason, requestedQuantity } = event.data;
console.warn(`follow ${relationshipId} skipped ${requestedQuantity} on ${tradingPairId}: ${reason}`);
break;
}
}
},
// Subscribe-time snapshot: every live follow, with change "snapshot".
(follows) => setFollows(follows),
);
unsubscribe();
```
Each relationship frame carries the follow's **full current state**, so replace the stored entry rather than merging. `version` is the durable-log sequence of the step that produced it: for one follow, higher is newer, so ignore a frame older than the one you hold. `status` is `ACTIVE`, `CLOSE_ONLY` or `STOPPED` (terminal); `change` names why the frame was sent (`created`, `updated`, `close_only`, `stopped`, `skip_recorded`, `reactivated`, or `snapshot`). A skip's `reason` is `INSUFFICIENT_MARGIN`, `BELOW_MIN`, `LEVERAGE_MISMATCH`, `MARKET_STATE`, `LEADER_EQUITY` or `NO_LIQUIDITY`; only `INSUFFICIENT_MARGIN` advances `consecutiveSkips`. The label fields are typed `KnownAlias | (string & {})`, so keep a `default` branch.
`cumNetRealized` and `hwm` are the follow's realized PnL net of fees since `startedAt` and its high-water mark — the basis for the leader's profit share. They are **not** the copy account's balance.
The snapshot carries live follows only and no skips, so a later snapshot (after a reconnect) is the complete live set: replace your map with it. Read `sdk.copyTrading.getFollow(followId)` for a follow's persisted recent skips.
### Copy Event Structure
```typescript theme={null}
type CopyEvent =
| {
eventType: "copy_relationship_update";
userId: string; // the follower (you)
data: {
relationshipId: string; // the follow id (Follow.followId over REST)
leaderWallet: string;
status: CopyRelationshipStatus | (string & {});
marginModePolicy: CopyMarginModePolicy | (string & {});
copyRiskBucketId: string; // the copy account's cross risk bucket
sizingMode: CopySizingMode | (string & {});
sizingParam?: string;
leverageMode: CopyLeverageMode | (string & {});
fixedLeverage?: string;
allocation: string;
slPct?: string;
copyExisting: boolean;
tradingPairIds: string[]; // empty: every market the leader trades
cumNetRealized: string; // profit-share basis, not a balance
hwm: string;
consecutiveSkips: number;
startedAt: string;
stoppedAt?: string;
stopReason?: CopyStopReason | (string & {});
change: CopyRelationshipChange | (string & {});
version?: number;
};
}
| {
eventType: "copy_skip";
userId: string;
data: {
relationshipId: string;
leaderWallet: string;
leaderTradeId: string;
leaderOrderId: string;
tradingPairId: string;
reason: CopySkipReason | (string & {});
requestedQuantity: string; // base units
consecutiveSkips: number;
detail?: string;
version?: number;
};
};
```
## Account Channel
Stream account-level margin health for the authenticated user — equity, collateral, free collateral, margin ratio, and maintenance requirement, per margin account. **Requires authentication**; the signed session-key handshake is sent before the subscribe.
The server-side producer that emits live `account_update` frames now runs on every network, `mainnet` included. Enablement is a deployment switch rather than a delivery guarantee, so confirm you are receiving frames before you build on a live stream. The subscribe-time snapshot below is served by the WebSocket API rather than that producer, so it arrives regardless. See the [WebSocket reference](/reference/websockets#account).
Detected lag closes the whole socket with 1013 under the [slow-client rule](#slow-client-disconnect-close-code-1013). Each frame carries the account's full current health, so consume it as a terminal-state snapshot per `marginAccountId`, never as an additive delta. Dedupe LIVE frames per margin account by `updatedAt`, newest wins; use `sequence` only to break ties between frames carrying the same `updatedAt` (those come from the same producer run). A snapshot row is not ranked this way — any live frame supersedes it, see [Account Snapshot](#account-snapshot). Never treat `sequence` as a cross-restart order: it resets to 0 when the producer restarts, which can happen while your socket stays connected. It may also skip values because publishing and delivery are at-most-once. A sequence gap alone is not a replay cursor; an explicit 1013 close still requires reconciliation of potentially missing account keys. A subscribe-time snapshot arrives ahead of the live frames, so no REST call is needed for a baseline — see [Account Snapshot](#account-snapshot) below.
### Flat accounts
Live frames flow on a \~2s tick for every account holding an open position or a resting order reserve. When its last position and reserve clear, one final frame carries the released state, and after that a flat account sends no keepalive. It still gets a frame after its collateral moves: a transfer between the spot wallet and margin, an on-chain deposit into margin or a withdrawal from it, a transfer between your margin accounts, closing a sub-account, a copy-trading allocation, or an insurance payout. Moves that land before the producer's next tick collapse into one frame carrying the latest state, and that frame normally arrives on the next tick, within \~2s of the move. The producer reads at most 512 flat accounts per tick, so a burst wider than that spreads over the following ticks. The frame carries the account's current collateral and uses the same per-account `sequence` as every other live frame, so the dedupe rule above applies unchanged. For a flat account with no open risk bucket, `equity` and `freeCollateral` equal `collateral`, and `maintenanceRequirement` and `marginRatio` are `"0"`. On-chain deposits and withdrawals also appear on the `movements` channel.
### Subscribe to Account Updates
```typescript theme={null}
const unsubscribe = sdk.ws.account(
(event) => {
const { marginAccountId, sequence, equity, freeCollateral, marginRatio, maintenanceRequirement } = event.data;
console.log(`${marginAccountId} seq=${sequence} equity=${equity} free=${freeCollateral} ratio=${marginRatio} mmr=${maintenanceRequirement}`);
},
// Subscribe-time snapshot: one row per margin account you own in THIS
// application, ahead of the live frames (live delivery is per user and
// wider, so do not treat an absent account as deleted). `sequence` is absent
// on these rows, and any live frame supersedes one.
(accounts) => {
for (const account of accounts) {
console.log(`baseline ${account.marginAccountId} equity=${account.equity} ratio=${account.marginRatio}`);
}
},
);
// Cleanup
unsubscribe();
```
### Account Snapshot
On subscribe, before any live frame, the server sends one row per margin account you own **in the application you authenticated under** — **including flat accounts**, whose live frames are rare: the producer's periodic frames cover only accounts holding an open position or a resting order reserve, and a flat account gets a live frame only when its collateral moves (see [Flat accounts](#flat-accounts)). Each row carries that account's current persisted health. An account that is flat **and otherwise empty** — no positions and no open risk bucket — reports its deposited collateral as both `equity` and `freeCollateral`, with a zero `maintenanceRequirement` and a zero `marginRatio`. An account that is flat but still holds an open risk bucket keeps that bucket's allocated collateral and unsettled realized PnL, so its `equity` need not equal its `collateral`. Owning no margin account delivers an empty array rather than no callback at all, so "you have none" is distinguishable from "this channel has no snapshot".
**A live frame always supersedes a snapshot row for the same `marginAccountId`, whatever the timestamps say.** Use `updatedAt` to order live frames against each other — not to rank a snapshot row against one. The snapshot is a PostgreSQL-backed baseline stamped when the state was *persisted*, while a live frame is stamped when the engine *sampled* it; under persistence lag a later-written row can carry older state, so the two clocks are not comparable. Treat the snapshot as a seed that the first live frame for that account replaces. That includes frames emitted *before* your subscription activated: the connection buffers them while the snapshot read runs and delivers them afterwards, so one can supersede a newer snapshot row. A later frame normally corrects it within the \~30s keepalive; the exception is an account that went flat in that window whose final frame never reached you, which leaves a stale active account until you reconnect and re-seed. A durable per-account version would fence this — until it exists, reconnecting is the recovery.
The live stream is slightly wider than the snapshot: `account_update` frames are delivered per user, so if the same wallet trades through more than one application you also receive live frames for those accounts. Each frame is full current health, so such an account baselines itself from its next frame (within the \~30s keepalive) even though the snapshot does not list it.
A snapshot row is the live payload with **`sequence` omitted**. The counter belongs to the matching engine's producer process and the WebSocket API cannot mint one; a `0` would be worse than absent, because it is indistinguishable from a restarted producer's first frame. Its absence costs you nothing, because a snapshot row is never ranked against a live frame by `updatedAt` or `sequence` in the first place — any live frame for that account supersedes it.
### Account Event Structure
```typescript theme={null}
{
eventType: "account_update";
userId: string;
data: {
marginAccountId: string; // margin account this update describes
equity: string; // collateral + unrealized PnL + funding (decimal string)
collateral: string; // raw deposited collateral in USD (decimal string)
freeCollateral: string; // equity headroom for existing cross exposure (decimal string); NOT the withdrawal bound — read withdrawableCollateral from the margin-accounts or portfolio-margin API
marginRatio: string; // maintenance requirement / equity (decimal string). "0" for an account with no open positions. Saturates to a maximum-distress sentinel (Decimal::MAX, ~7.9e28) whenever the ratio cannot be computed or represented — a maintenance requirement at zero/negative equity, or near-zero positive (dust) equity where the true ratio exceeds the decimal range — treat a value at/above the liquidation threshold as maximal distress and clamp it for display
maintenanceRequirement: string; // maintenance margin required to stay solvent (decimal string)
sequence?: number; // per-account counter within one producer run, LIVE frames only: dedupe live frames by updatedAt (newest wins), sequence tie-breaks same-updatedAt frames. Resets to 0 on producer restart (socket may stay connected), MAY skip values — never a gap detector or cross-restart order. ABSENT on a subscribe-time snapshot row, which any live frame supersedes regardless of timestamps; treat absent as unknown, never as zero
updatedAt: string; // ISO 8601 timestamp (UTC)
}
}
```
## TWAP Orders Channel
Stream live progress for your [TWAP parent orders](/trading-mechanics/twap-orders). **Requires authentication.** Subscribe to every TWAP parent you own, or scope to one market with the optional `tradingPairId`. The channel is private — only the owner receives events — and delivers the **full parent snapshot** on every lifecycle transition (creation, each placed or skipped slice, cancellation, completion, the passive-child mutations of a `PASSIVE` parent, and the activation or expiry of a conditional one), tagged with the reason for the update, so a client renders live progress without polling.
### Subscribe to TWAP Orders
```typescript theme={null}
const unsubscribe = sdk.ws.twapOrders((event) => {
const t = event.data;
console.log(
`${t.twapOrderId} ${t.state} ` +
`executed=${t.executedQuantity}/${t.totalQuantity} ` +
`slices=${t.slicesPlaced}/${t.plannedSlices} (${t.reason})`
);
});
// Or scope to one market
const unsubPair = sdk.ws.twapOrders(handler, pair.id);
// Cleanup
unsubscribe();
```
### TWAP Order Event Structure
```typescript theme={null}
import type {
OrderSide,
TradingMode,
TwapExecutionStyle,
TwapOrderEventReason,
TwapOrderState,
} from "@0xmonaco/types";
{
eventType: "twap_order_update";
userId: string;
data: {
twapOrderId: string;
version?: number; // opaque same-parent reconciliation revision; absent when unknown
tradingPairId: string;
side: OrderSide | (string & {});
tradingMode: TradingMode | (string & {});
totalQuantity: string;
executedQuantity: string;
executedNotional: string;
averageFillPrice?: string; // absent until the first fill
plannedSlices: number;
slicesPlaced: number;
slicesSkipped: number; // refusals, not slots — a PASSIVE slot can increment this AND slicesPlaced; skips never terminate the parent
nextSliceAt?: string;
state: TwapOrderState | (string & {});
terminalReason?: string; // e.g. end-of-window shortfall, or "trigger expired" (human-readable, not a parsing contract)
executionStyle: TwapExecutionStyle | (string & {});
triggerPrice?: string; // conditional parents only
triggerDirection?: "ABOVE" | "BELOW" | (string & {});
conditionalExpiry?: string; // deadline for the trigger to fire
triggeredAt?: string; // when the trigger fired
reason: TwapOrderEventReason | (string & {}); // what triggered this frame — see Reasons below
// plus config echoes (limitPrice, slippageToleranceBps, reduceOnly, margin context,
// clientTwapId, randomize, startAt/endAt) and created/updated/cancelled/completed timestamps
}
}
```
**Reasons:**
| `reason` | Meaning |
| - | - |
| `created` | The parent was accepted. |
| `slice_placed` | A slice reached the book as a taker — a `TAKER` slot that placed, plus a `PASSIVE` parent's deadline sweeps and crossing takes. A slot that placed nothing generally reports `slice_skipped` instead, so this is not every slot — with one exception: a child stopped by [self-trade prevention](/developers/trade#self-trade-prevention) under `CANCEL_TAKER` **or** `CANCEL_BOTH` comes back `CANCELLED` rather than rejected, and TWAP counts any non-`REJECTED` child as placed — so it increments `slicesPlaced`, not `slicesSkipped`. Its recorded fill is the child's actual `total_filled`, which is **not necessarily zero**: third-party fills made before the first self-touch stand, and only a self-touch before any fill leaves it at zero. |
| `slice_skipped` | A slice attempt was **refused** (no liquidity in band, parent limit through the market, halted market, failed admission). Never terminal — watch `slicesSkipped`. Note the counters are refusal and placement counts, not disjoint slot totals: a `PASSIVE` slot that already posted a maker child (counted in `slicesPlaced`, possibly with fills) and whose deadline sweep is then refused increments **both**, so `slicesPlaced + slicesSkipped` can exceed the slots consumed. |
| `passive_child_placed` | `PASSIVE` only: a post-only child now rests at the touch. |
| `child_repriced` | `PASSIVE` only: the reprice step ran. Usually the resting child moved atomically to the new touch — but the same reason is emitted by the cancel-only fallback, where the old child was cancelled and the repost did **not** happen, leaving no child resting and the slot open for the next attempt or the deadline sweep. The two cases are indistinguishable on this channel — `TwapOrderEventData` has no active-child field — so treat resting-child status as unknown here and track the child on the order channels instead. |
| `passive_fill` | `PASSIVE` only: maker fills on the resting child, folded into the parent — **except the fill that completes the parent**, which reports `completed` instead. A client waiting for a final `passive_fill` waits forever. |
| `triggered` | Conditional only: the trigger fired and the window armed. Read `startAt` / `endAt` / `triggeredAt` off this frame. |
| `trigger_expired` | Conditional only: `conditionalExpiry` passed with the trigger unfired. Terminal `CANCELLED`, nothing executed; the formatted detail `trigger expired` rides `terminalReason`. |
| `cancelled` | The owner cancelled. A `PASSIVE` parent's resting child is removed in the same step — earned fills stand, no final sweep. |
| `completed` | Terminal: the full quantity executed, or the window closed with the remainder reported as shortfall. Takes precedence over `passive_fill` when a maker fill is what completes the parent. |
| `snapshot` | Not a live mutation — the subscribe-time baseline row for an owned non-terminal (`PENDING`/`ACTIVE`) parent, delivered once through `onSnapshot`. `reason` is free-form on the wire, so `TwapOrderEventReason` narrows the live values above and `snapshot` arrives through the `(string & {})` widening. |
### Subscribe-time snapshot
Pass an `onSnapshot` callback — the third argument, after the optional `tradingPairId` — to receive the current baseline as one `TwapOrderEventData[]`, injected ahead of the live frames so no REST read is needed to seed state:
```typescript theme={null}
sdk.ws.twapOrders(
(event) => applyTwapUpdate(event.data), // live diffs
undefined, // all pairs (or pass a tradingPairId)
(parents) => setTwapParents(parents), // complete current state, once
);
```
Each row is the same shape as a live frame with `reason: "snapshot"`. The baseline carries only the owned **non-terminal** parents — `PENDING` (awaiting its start window, or, for a conditional parent, still awaiting its price trigger — which can be indefinite) and `ACTIVE` (triggered/started and slicing) — optionally scoped to the filtered pair; an owner with no live parents receives `onSnapshot([])`, never a skipped call. Terminal parents (`COMPLETED` / `CANCELLED`) are history and never appear — read those from `listTwapOrders`. The shared rules — one baseline per wire subscription, all-or-nothing delivery, and a fresh baseline after every reconnect — are covered under [Subscribe-time snapshots](#subscribe-time-snapshots).
Merge live frames and snapshot rows for the same `twapOrderId` by `version`:
accept `>=`, ignore a lower version, and let a snapshot win an equal-version
tie. Versions are opaque and may skip. An absent version means unknown legacy
state, not zero; fall back to a full-field comparison or REST reconciliation.
Never compare versions across different TWAP parents.
Treat the baseline as a **seed, not an authoritative replacement set**. Like the persisted `orders` snapshot it is PostgreSQL-backed, so a parent that reached a terminal state just before you connected can still arrive as `ACTIVE`/`PENDING` if that transition has not yet been persisted — and its terminal event, sent before this subscription existed, does not re-fire. Absence from the baseline is likewise not a tombstone. So merge the snapshot into local state rather than replacing it wholesale (the `setTwapParents` above is the first-connection seed), and reconcile a parent that then stays live with no further frames against the REST TWAP read.
Each child order is a regular order, so its lifecycle flows through the standard [Orders Channel](#orders-channel) above, and every one of its frames names the parent in `parentOrderId` (the same field the order read model carries):
* A **taker slice** (a `TAKER` slot, or a `PASSIVE` parent's deadline sweep or crossing take) sends `OrderPlaced`, then its taker fill, then an `OrderCancelled` for the remainder when the price band stops it short. Each resting order it fills receives its maker fill exactly as it would from any other taker. Its fills reach the other channels as any taker's do: one public trade per fill on `trades`, the `orderbook` update and the live candle on `ohlcv`, and balance and position updates for the owner and for every resting order it filled, including the closing position update when the slice closes a position.
* A **passive child** sends `OrderPlaced` when it rests and a maker fill each time another order trades against it. It sends `OrderCancelled` when it leaves the book: with `terminalReason` `REPLACED` when a reprice or the slot's deadline sweep replaces it, and `USER_REQUESTED` when the parent is cancelled or reaches its end time. On a reprice, the old child's `OrderCancelled` arrives before the new child's `OrderPlaced`, so tracking the child here resolves the `child_repriced` ambiguity above. Each time a passive child rests or leaves the book, the `orderbook` channel gets an update, and when a spot child leaves the book its owner gets the same `user_movement` frame for the released lock that cancelling the order by hand sends.
Every frame one TWAP step produces carries that step's `version`. A slice that places nothing sends no order frame for the child itself. A resting order that a TWAP slice cancels, for self-trade prevention, because it failed its risk re-check during the match, or because the slice closed the position it reduced, receives an `OrderCancelled` frame in the same step, as it would from any other order, even when the slice itself is refused. A spot order's released lock also arrives on the movements channel.
Forced closes arrive on the same [Orders Channel](#orders-channel), each frame carrying the `version` of the step that produced it:
* **Resting orders cancelled by a liquidation.** When a risk bucket enters liquidation, each of its resting orders receives an `OrderCancelled` with `terminalReason` `LIQUIDATION`.
* **Reduce-only orders cancelled by auto-deleveraging.** When ADL fully closes a counterparty's position, each of that risk bucket's resting reduce-only orders on the pair receives an `OrderCancelled` with `terminalReason` `AUTO_DELEVERAGING`; the counterparty was deleveraged, not liquidated.
* **Liquidation orders.** The engine's liquidation order sends its owner `OrderPlaced`, then its taker fill, then an `OrderCancelled` for any remainder. A liquidation order the book could not fill at all sends `OrderPlaced` then `OrderRejected` instead, or no frame if it was cancelled without a fill. Each resting order it fills receives its maker fill exactly as it would from any other taker.
* **Auto-deleveraging (ADL).** The bankrupt account's close order sends its owner one `OrderFilled` per ADL step. Each account ADL closes against receives the fill frames for the close order recorded against its position, exactly as a resting order receives them from any other taker: one per trade, each at that trade's price, the last being `OrderFilled`. A position closed partly at the bankruptcy price and partly at the mark price therefore receives an `OrderPartiallyFilled` and then an `OrderFilled`. Those frames arrive only when the whole ADL episode ends, not as each step executes, and still carry the `version` of the step that filled them, so the usual `>=` merge applies. If the matching engine restarts during an episode, they are not sent; read the orders with `getOrder`.
A resting order that a liquidation order cancels, for self-trade prevention, a failed risk re-check, or because the fill closed the position it reduced, receives an `OrderCancelled` frame in the same step.
## Market Stats Channel
Stream periodic market statistics. **Public** — no session handshake required. Detected lag closes the whole socket with 1013 under the [slow-client rule](#slow-client-disconnect-close-code-1013). Each frame carries the market's full current stats, so consume it as a terminal-state snapshot rather than a delta.
Two subscriptions: `sdk.ws.marketStats(pairId, handler)` for one market's full stats, and `sdk.ws.marketStatsAll(handler)` for a slower-cadence reduced summary of every published market in a single frame (a market that has not yet assembled its first frame is omitted; the subscribe-time snapshot carries the same set). Perp-only fields (index/mark price, open interest, funding) are **omitted for spot markets** and surface as `undefined`. Until a perp's open interest is known, `openInterest` and `openInterestHeadroom` are `undefined` (never `"0"`) while every other field still arrives; once known, every frame carries the most recent value. Every per-market frame carries `marketStatus`, the same label as that market's all-markets row. Perp frames also carry `fundingIntervalSeconds` and `lastFundingTime`; `lastFundingTime` is a millisecond epoch number (REST `getFundingState` returns it as an RFC 3339 string) and is `undefined` until the market's first settlement. Each perp row of `marketStatsAll` also carries `indexPrice`, `fundingRate` (the estimated next-window rate; `currentFundingRate` is the last settled one), `premium`, and `openInterestNotional`: `openInterest` priced at the mark, falling back to the last trade price when there is no mark, in quote units. `openInterestNotional` is `undefined` (never `"0"`) until both open interest and a price are known.
Both accept an optional [`onSnapshot`](#subscribe-time-snapshots) that delivers the current baseline before the live frames: `marketStats` yields a one-element `MarketStatsData[]` with that pair's latest complete frame, and `marketStatsAll` yields a `MarketStatsSummary[]` with one summary per active market with retained data, deterministic by trading-pair id. Prefer it on reconnect over waiting for the next periodic push. A per-market subscription whose market has not yet produced a frame — or on a transient read failure — receives a `SNAPSHOT_UNAVAILABLE` signal rather than a fabricated one; a later published live frame seeds it once the market produces one. `marketStatsAll` can likewise return `SNAPSHOT_UNAVAILABLE` for a brief window during initial startup or after a server restart (until the producer commits its first full generation), or on a transient snapshot read failure, rather than a partial list; handle the coded error as a resync signal (the next `marketStatsAll` push delivers the baseline) rather than assuming `onSnapshot` always fires. Where a deployment does not run the market-stats snapshot service, neither form sends a snapshot or a `SNAPSHOT_UNAVAILABLE` signal at all — the first live push seeds state, so never block on `onSnapshot` firing.
### Subscribe to Market Stats
```typescript theme={null}
// Per-market: full stats for one trading pair
const unsubscribe = sdk.ws.marketStats(tradingPairId, (event) => {
const { markPrice, openInterest, currentFundingRate, dailyPriceChangePercent } = event.data;
console.log(`${event.tradingPairId} mark=${markPrice} OI=${openInterest} funding=${currentFundingRate} 24h%=${dailyPriceChangePercent}`);
});
// All markets: one summary row per market, per frame
const unsubscribeAll = sdk.ws.marketStatsAll((event) => {
for (const m of event.markets) {
console.log(`${m.symbol} ${m.marketStatus} last=${m.lastTradePrice} 24h%=${m.dailyPriceChangePercent}`);
}
});
// Cleanup
unsubscribe();
unsubscribeAll();
```
### Market Stats Event Structures
```typescript theme={null}
import type { MarketStatus } from "@0xmonaco/types";
// Per-market (market_stats:)
{
tradingPairId: string;
data: {
symbol: string;
tradingPairId: string;
indexPrice?: string; // perp-only; omitted for spot
markPrice?: string; // perp-only; omitted for spot
midPrice?: string;
bestBidPrice?: string;
bestAskPrice?: string;
openInterest?: string; // perp-only; omitted for spot and until known
openInterestLimit?: string; // perp-only; USD notional OI cap, omitted when uncapped
openInterestHeadroom?: string; // perp-only; cap - OI x mark (>= 0), omitted when uncapped or until OI is known
lastTradePrice?: string;
dailyBaseTokenVolume?: string;
dailyQuoteTokenVolume?: string;
dailyPriceLow?: string;
dailyPriceHigh?: string;
dailyPriceChange?: string;
dailyPriceChangePercent?: string;
currentFundingRate?: string; // perp-only; omitted for spot
fundingRate?: string; // perp-only; omitted for spot
fundingTimestamp?: number; // ms epoch; perp-only; omitted for spot
premium?: string; // perp-only; omitted for spot
baseInterestRate?: string; // perp-only; omitted for spot
fundingClampSmall?: string; // perp-only; omitted for spot
fundingClampBig?: string; // perp-only; omitted for spot
marketStatus?: MarketStatus | (string & {}); // same label as the all-markets row; always sent
fundingIntervalSeconds?: number; // whole seconds; perp-only; omitted for spot
lastFundingTime?: number; // ms epoch of the latest settlement; perp-only; omitted for spot and before the first settlement
}
}
// All markets (market_stats)
{
markets: Array<{
tradingPairId: string;
symbol: string;
lastTradePrice?: string;
markPrice?: string; // perp-only; omitted for spot
dailyPriceChange?: string;
dailyPriceChangePercent?: string;
dailyBaseTokenVolume?: string;
dailyQuoteTokenVolume?: string;
dailyPriceHigh?: string;
dailyPriceLow?: string;
openInterest?: string; // perp-only; omitted for spot and until known
currentFundingRate?: string; // perp-only; omitted for spot
fundingTimestamp?: number; // ms epoch; perp-only; omitted for spot
marketStatus: MarketStatus | (string & {});
indexPrice?: string; // perp-only; omitted for spot
fundingRate?: string; // estimated next-window rate; perp-only; omitted for spot
premium?: string; // perp-only; omitted for spot
openInterestNotional?: string; // quote units; perp-only; omitted for spot and until open interest and a price are known
}>;
}
```
## Instruments Channel
Stream market lifecycle and configuration changes: new listings, delistings, and edits to tick size, quantity step, order-size bounds, or category. **Public** — no session handshake required. Detected lag closes the whole socket with 1013 under the [slow-client rule](#slow-client-disconnect-close-code-1013). On reconnect, apply the new subscribe snapshot; if unavailable, reconcile over REST rather than waiting for another event from every market.
Every frame carries the market's **full current configuration** as a terminal-state snapshot — never an additive delta — with `changedFields` naming which fields the mutation touched, for handlers that want to react selectively.
The list of every listed market arrives **before** the live diffs, so a subscriber always starts from a complete baseline. It includes inactive markets flagged `isActive: false`, so positions, orders and history on a delisted market can still resolve its metadata; filter on `isActive` for the tradable set. Markets not yet launched are not included. How it is delivered depends on whether you pass [`onSnapshot`](#subscribe-time-snapshots): with it, the list arrives as one `InstrumentEventData[]`; without it, as one `listing` event per market through the ordinary handler. Prefer `onSnapshot` if you keep a market map — it is the only form that tells you where the baseline ends, which is what lets you replace the map after a reconnect: a market delisted while the socket was down arrives flagged `isActive: false`, and a market no longer listed drops out. A subscriber joining a channel this client already holds is replayed the current baseline from a local cache instead, through whichever form it registered (the server snapshots only genuinely new wire subscriptions).
Omit `tradingPairId` to receive every market, or pass one to filter.
### Subscribe to Instruments
```typescript theme={null}
// All markets
const unsubscribe = sdk.ws.instruments((event) => {
const c = event.data;
console.log(`${c.symbol} ${c.change} active=${c.isActive} tick=${c.tickSize} (${c.changedFields.join(", ")})`);
});
// Or scope to one market
const unsubPair = sdk.ws.instruments(handler, pair.id);
// With the baseline as one array, so the market map can be replaced wholesale
const unsubWithSnapshot = sdk.ws.instruments(handler, undefined, (markets) => {
setMarketsByPair(Object.fromEntries(markets.map((market) => [market.tradingPairId, market])));
});
// Cleanup
unsubscribe();
```
### Instrument Event Structure
```typescript theme={null}
import type { InstrumentChangeKind, RiskTier } from "@0xmonaco/types";
{
tradingPairId: string;
data: {
change: InstrumentChangeKind | (string & {});
tradingPairId: string;
symbol: string; // e.g. "BTC/USDC"
isActive: boolean; // whether the market is currently tradable
tickSize: string; // decimal string
quantityStepSize: string; // decimal string
minOrderSize: string; // decimal string
maxOrderSize: string; // decimal string
marketType: string; // e.g. "spot" or "margin"
category: string; // e.g. "crypto", "equities", "commodities"
baseAssetId?: string; // base asset UUID
quoteAssetId?: string; // quote asset UUID
baseToken?: string; // base ticker, e.g. "BTC"
quoteToken?: string; // quote ticker, e.g. "USDC"
baseAssetName?: string; // base asset display name
quoteAssetName?: string; // quote asset display name
baseDecimals?: number; // base asset decimals (integer)
quoteDecimals?: number; // quote asset decimals (integer)
baseIconUrl?: string | null; // base asset icon URL; null when none configured
quoteIconUrl?: string | null; // quote asset icon URL; null when none configured
minLeverage?: string; // perp-only; omitted for spot
maxLeverage?: string; // perp-only; omitted for spot
initialMarginRate?: string; // perp-only; omitted for spot
maintenanceMarginRate?: string; // perp-only; omitted for spot
riskTiers?: RiskTier[]; // perp-only margin ladder, ascending; omitted for spot
changedFields: string[]; // snake_case wire names of the fields this mutation touched (empty on baseline frames)
updatedAt: string; // ISO 8601 timestamp (UTC)
}
}
```
Perp frames carry the market's whole margin ladder as `riskTiers`, ascending by `tierLevel`, in the same `RiskTier` shape and values as the REST perp config's `riskTiers`; `maxLeverage`, `initialMarginRate` and `maintenanceMarginRate` are tier 1's values. A margin-ladder edit that changes any tier, or the position cap that bounds the top tier, emits a `config_change` whose `changedFields` includes `"risk_tiers"` (plus the three tier-1 fields when tier 1 changed); saving an unchanged ladder emits nothing, and neither does a fee edit. `riskTiers` is optional on the type because a server that predates it omits it. `baseIconUrl` and `quoteIconUrl` apply to every market and are always present (unlike the perp-only fields, which are omitted for spot), each `null` when that asset has no icon — join a position's `tradingPairId` to the instrument row to render both icons without a REST call. Every market's frame also carries its asset identity (`baseAssetId`, `quoteAssetId`, `baseToken`, `quoteToken`, `baseAssetName`, `quoteAssetName`, `baseDecimals`, `quoteDecimals`), so you can label a market and scale its amounts without parsing `symbol` or calling REST; perp frames add `minLeverage`. These fields are optional on the type because a server that predates them omits them. `halt` / `unhalt` are reserved for a future market-regime producer and are not emitted by admin trading-pair mutations. For the raw wire protocol (snake\_case keys, the `Snapshot` envelope), see the [WebSocket reference](/reference/websockets#instrument).
## Complete Example
```typescript theme={null}
import { MonacoSDK } from "@0xmonaco/core";
import { createWalletClient, http } from "viem";
import { privateKeyToAccount } from "viem/accounts";
import { seiTestnet } from "viem/chains";
async function setupRealtime() {
const account = privateKeyToAccount("0x...");
const walletClient = createWalletClient({
account,
chain: seiTestnet,
transport: http()
});
const sdk = new MonacoSDK({
walletClient,
network: "staging",
seiRpcUrl: "https://evm-rpc-testnet.sei-apis.com",
});
// Login for orders channel
await sdk.login("your-client-id");
// Get trading pair
const pair = await sdk.market.getTradingPairBySymbol("BTC/USDC");
// Subscribe to all channels
const unsubOrders = sdk.ws.orders(pair.id, "SPOT", (event) => {
console.log("Order:", event.eventType, event.orderId);
});
const unsubOrderbook = sdk.ws.orderbook(pair.id, "SPOT", 1, "BASE", (event) => {
console.log("Spread:", parseFloat(event.bestAsk) - parseFloat(event.bestBid));
});
const unsubOHLCV = sdk.ws.ohlcv(pair.id, "SPOT", "1m", (event) => {
console.log("Price:", event.candlestick.c);
});
const unsubTrades = sdk.ws.trades(pair.id, (event) => {
console.log("Trade:", event.data.price, event.data.quantity);
});
const unsubMovements = sdk.ws.movements((event) => {
console.log("Movement:", event.data.transactionType, event.data.amount);
});
// Cleanup when done
return () => {
unsubOrders();
unsubOrderbook();
unsubOHLCV();
unsubTrades();
unsubMovements();
sdk.ws.disconnect();
};
}
```
## React Integration
```tsx theme={null}
import { useEffect, useState } from "react";
import { useMonacoSDK } from "@0xmonaco/react";
import type { OrderEvent } from "@0xmonaco/types";
function OrderFeed({ tradingPairId }: { tradingPairId: string }) {
const { sdk } = useMonacoSDK();
const [events, setEvents] = useState([]);
useEffect(() => {
if (!sdk) return;
const unsubscribe = sdk.ws.orders(tradingPairId, "SPOT", (event) => {
setEvents((prev) => [event, ...prev].slice(0, 50));
});
return () => unsubscribe();
}, [sdk, tradingPairId]);
return (
{events.map((event, i) => (
-
{event.eventType}: {event.orderId}
))}
);
}
```
## Best Practices
### 1. Use Trading Pair IDs
All WebSocket channels use trading pair IDs (UUIDs), not symbols:
```typescript theme={null}
// Get the pair first
const pair = await sdk.market.getTradingPairBySymbol("BTC/USDC");
// Use the UUID
sdk.ws.orders(pair.id, "SPOT", handler);
```
### 2. Always Clean Up
Store unsubscribe functions and call them when done:
```typescript theme={null}
const unsubscribe = sdk.ws.orders(pairId, "SPOT", handler);
// Later
unsubscribe();
```
### 3. Authenticate for Orders
The orders channel requires authentication:
```typescript theme={null}
await sdk.login(clientId);
const unsub = sdk.ws.orders(pairId, "SPOT", handler);
```
### 4. Handle Reconnection
`sdk.ws` reconnects automatically (unlimited retries by default). Poll `getStatus()` if you need to surface it:
```typescript theme={null}
setInterval(() => {
if (sdk.ws.getStatus() !== "connected") {
console.warn("Reconnecting...");
}
}, 5000);
```
For an `onResync` hook and an `onStatusChange` callback, pass a `ws` config to the SDK (`new MonacoSDK({ ..., ws })`) or construct the client with `createMonacoWebSocket` — see [Reconnection and resync](#reconnection-and-resync).
# Sub-Accounts
Source: https://docs.0xmonaco.com/sub-accounts
Run separate strategies from separate margin accounts.
Sub-accounts are extra margin accounts that sit beside your parent margin account. Each one holds its own collateral and its own positions, all under the same wallet.
## Why Use Sub-Accounts
Sub-accounts are **isolated**. A loss in one sub-account never draws on another, or on your parent margin account.
Use them to keep a hedge apart from your main book, to run one strategy per account, or to give a bot or agent an account of its own.
## Funding A Sub-Account
| Movement | How it works |
| - | - |
| Deposit / withdrawal | On-chain, through your parent margin account only. |
| Parent → sub-account | Internal transfer. Gasless, with no on-chain transaction. |
| Sub-account → parent | Internal transfer. Gasless, with no on-chain transaction. |
Withdraw from your parent margin account.
To withdraw funds held in a sub-account, transfer them back to your parent margin account first.
## Limits
| Rule | Value |
| - | - |
| Open sub-accounts | 10 at the first volume tier, rising with your 14-day trading volume to 50. |
| Copy sub-accounts | Do not count toward the limit. |
| Names | Up to 64 letters, digits, `_`, `-`, `.` or `:`. |
## Closing A Sub-Account
A sub-account can be closed once it is **flat**: no open positions and no resting, conditional, or TWAP orders. Its collateral returns to your parent margin account in the same step.
A sub-account still in liquidation or bad debt cannot be closed.
## Agents And Copy Trading
A delegated agent can be limited to a single sub-account, so it trades only that account.
Following a lead trader opens a copy sub-account for that leader, named `copy:` followed by the leader's ID. Names starting with `copy:`, in any letter case, are reserved for these accounts. See [Copy Trading](/developers/copy-trading).
## Working With Sub-Accounts
[Perps Collateral](/developers/perps-collateral#margin-sub-accounts) covers creating, funding, reading, and closing sub-accounts through the API and SDK. The [Margin Accounts SDK](/sdk/typescript/margin-accounts) lists every margin account method, and [Positions](/developers/positions) covers position risk.
API Surface
| Action | REST | TypeScript SDK |
| - | - | - |
| Create (or reopen) | `POST /api/v1/margin/sub-accounts` | `createMarginSubAccount({ subaccountKey })` |
| List | `GET /api/v1/margin/accounts` | `listMarginAccounts()` |
| Transfer in | `POST /api/v1/margin/sub-accounts/{marginAccountId}/collateral/transfer-in` | `transferCollateralToSubAccount(marginAccountId, amount)` |
| Transfer out | `POST /api/v1/margin/sub-accounts/{marginAccountId}/collateral/transfer-out` | `transferCollateralFromSubAccount(marginAccountId, amount)` |
| Close | `POST /api/v1/margin/sub-accounts/{marginAccountId}/close` | `closeMarginSubAccount(marginAccountId)` |
`POST /api/v1/margin/collateral/transfer` (`transferMarginCollateral`) also moves collateral between any two of your margin accounts. Rows from `listMarginAccounts()` carry `subaccountKey` and `marginAccountState`.
Transfer Rules
* Creating, closing, and transferring share the per-user movement budget with withdrawals. Exceeding it returns `429` with `retryAfter`.
* The sub-account transfer routes refuse your parent margin account with `400`; use the parent margin account routes for it.
* An unknown margin account, or one you do not own, returns `404`.
* A delegated agent session cannot use the sub-account transfer routes.
# Testnet Guide
Source: https://docs.0xmonaco.com/testnet-guide
Connect a wallet, get test USDC, and place your first trade.
Try Monaco with free test assets. This walkthrough takes you from connecting a wallet to finding and closing your first position.
Sei Atlantic-2 testnet
Test assets have no real-world value. Trading is gasless; you do not need SEI tokens for the setup below.
Open Monaco Testnet ↗
## 1. Connect Your Wallet
Open the terminal and choose **Connect Wallet** in the top-right corner. Use MetaMask, Rabby, or Coinbase Wallet, or sign in with email or socials to have a wallet created for you.
Privy remembers your wallet preference, so the first option shown may differ between users. After connecting, trades do not require a wallet signature popup.
## 2. Get Test USDC
Select the **droplet icon** in the top toolbar, then **Mint 2,000 USDC**. A green dot means a mint is available.
| Destination | Test funds per mint |
| - | - |
| Spot account | 1,000 USDC |
| Margin account | 1,000 USDC |
You can mint 2,000 USDC once per rolling 24 hours on each app. Your mint history appears in the same modal. If you run out, try again once 24 hours have passed since your last mint.
## 3. Place An Order
Choose a market from the ticker bar or press **⌘K** to search. Available perpetual markets include crypto, stocks, commodities, and FX, such as BTC, NVDA, GOLD, and EUR.
1. Choose **Market** for immediate execution or **Limit** to set a price.
2. Choose **Buy / Long** or **Sell / Short**.
3. Enter the size in the asset or USDC, or use the percentage slider.
4. Review the order value, required margin, and liquidation price, then select **Place Order**.
See [Orderbook](/trading-mechanics/spot) for execution behavior and price protection.
## 4. Find And Close Your Position
Your open position appears in **Positions** at the bottom of the terminal. This shows live PnL, entry price, liquidation price, and a **Close** button.
## After Your First Trade
**Portfolio** shows account equity, available balance, margin used, realized PnL, win rate, and your spot/margin breakdown. The balance pill at the top right opens the account panel for deposits, withdrawals, and transfers between spot and margin.
**Compete** contains your TraderCode and invite link. Leaderboards and rewards are coming soon.
The testnet guide lists taker fees of **0.045%** and a maker rebate of **0.01%**. A limit order can execute immediately against existing liquidity; see [maker and taker fills](/fees) for the distinction.
Testnet may experience downtime and service interruptions while backend and frontend functionality is updated.
Everything runs on testnet: free play money and zero risk. Connect a wallet, grab test USDC from the built-in faucet, and place your first trade.
[Open Monaco Testnet](https://trade.0xmonaco.com)
**Network:** Sei Atlantic-2 testnet · **Trading:** gasless · **Funds:** test assets with no real-world value
Open [trade.0xmonaco.com](https://trade.0xmonaco.com) and click **Connect Wallet** in the top-right corner.
Pick how you want to log in. Any option works — you don't need SEI tokens or an existing testnet setup:
* **MetaMask, Rabby, or Coinbase Wallet** — approve the connection in the wallet popup.
* **Email or socials** — no wallet extension needed; one gets created for you.
**Privy remembers your preference.** When Privy opens, it prioritizes the wallet you used most recently or use most often. The first wallet shown can therefore differ between users.
**No gas, ever.** After this one-time connection, everything on Monaco testnet is gasless — no signature popups when you trade.
Click the **droplet icon** in the top-right toolbar. It has a green dot when a mint is available.
Hit **Mint 2,000 USDC**. Each mint deposits 1,000 USDC to your spot account and 1,000 USDC to your margin account, ready to trade instantly.
**Daily limit:** One mint per rolling 24 hours on each app, for 2,000 USDC. If you run out, try again once 24 hours have passed since your last mint.
Pick a market from the ticker bar or press `⌘K` to search. Monaco lists crypto, stocks, commodities, and FX perpetuals, including BTC, NVDA, GOLD, and EUR.
1. Choose **Market** to fill immediately or **Limit** to set your price.
2. Pick **Buy / Long** or **Sell / Short**.
3. Enter a **Size** in the asset or USDC, or use the 25/50/75/100% slider.
4. Review order value, margin required, and liquidation price, then click **Place Order**.
Your open position appears in the **Positions** tab at the bottom, with live PnL, entry price, liquidation price, and a one-click **Close** button.
Click **Portfolio** in the top navigation for account equity over time, available balance, margin used, realized PnL, win rate, and your spot and margin account breakdown.
Your balance pill in the top-right corner opens a quick account panel at any time. Use it to deposit, withdraw, or move funds between spot and margin.
The **Compete** tab has your personal TraderCode and invite link. Share it to bring friends onto the testnet. Leaderboards and rewards are rolling out soon, so trades you make now are good practice.
Cheat sheet
| Task | Where or how |
| - | - |
| Sign in | **Connect Wallet** in the top right — use a wallet, email, or socials |
| Get free test USDC | Droplet icon in the top right, then **Mint 2,000 USDC**, once per day |
| Find markets | Use the ticker bar or press `⌘K` |
| Trade | Order ticket, then Market or Limit, Buy or Sell, Size, and **Place Order** |
| Manage positions | Bottom panel, then **Positions**, then **Close** |
| Review performance | **Portfolio** in the top navigation |
| Fees | Taker 0.045%; maker −0.01%, meaning limit orders receive a rebate |
| Gas | None needed after connecting; trading is gasless |
This is a testnet. All balances are testnet funds, with no real-world value. Expect occasional downtown and service interruptions as both backend and frontend functionality is updated and improved.
# Auto-Deleveraging
Source: https://docs.0xmonaco.com/trading-mechanics/auto-deleveraging
When the order book will not take a liquidating position, Monaco closes it against opposite-side holders at a price fixed when liquidation began — how the trigger works, who pays, in what order, and what reaches the insurance fund.
Auto-deleveraging (ADL) is the last rung of Monaco's liquidation waterfall. It exists for one situation: **the order book will not take a position at the price liquidation fixed for it**, so the only remaining counterparties are the traders holding the other side.
When liquidation begins, Monaco fixes a closing price for every position in the bucket — the price at which that bucket's equity reaches exactly zero. That price is **pinned**: it does not move afterwards, however far the mark travels. Everything below is priced against it.
The waterfall has three tiers:
1. **Protected liquidation** — reduce-only IOC limit orders, limited at the position's *protected rung*: three quarters of the way from its pinned price back toward solvency. Protects execution quality while the bucket still has price protection to spend.
2. **Terminal liquidation** — once the bucket falls below three quarters of its maintenance margin, reduce-only attempts for the full remaining position, limited at the pinned price or unlimited depending on the venue's liquidation mode. Protects solvency: the bucket is going to be closed either way, so the only question left is where.
3. **Auto-deleveraging** — whatever the book would not take, closed against traders holding the opposite side, at the pinned price.
See [Liquidations](/trading-mechanics/liquidations) for tiers 1 and 2.
ADL is the only mechanism here that reaches into an account that did nothing wrong. Monaco therefore states its rules plainly, in advance, rather than leaving them to be discovered during an incident.
## What Monaco does differently
Some exchanges rank traders by profitability and leverage, then close the single highest-ranked account first. That concentrates the entire cost of an event on one trader — usually the one who was most right about the market.
Monaco does not rank by profitability. It asks, for every trader on the opposite side, **where the fill would leave them** — counting only the collateral they could not withdraw anyway — and reaches everyone it leaves covered before anyone it would push past zero. That first group is filled pro-rata by position size, so the cost spreads across the market in proportion to exposure rather than landing on one account. Being the most profitable trader in a market never makes you first; being the one with nothing behind the position does.
## When ADL triggers
A risk bucket is auto-deleveraged when all three of the following hold:
* Auto-deleveraging is enabled for the venue.
* The bucket is **below three quarters of its maintenance margin**, so it has no price protection left to spend.
* **The book refused the position** on this attempt.
That last condition is the whole trigger. ADL is not a response to a number on a screen; it is a response to an order that did not fill.
### Why not bankruptcy
The obvious rule — deleverage once equity hits zero — measures the wrong thing, and it is worth being explicit about why, because most venues use it.
Equity is computed at the **mark**. When the book will not trade, the mark is a price nobody is honouring, so "solvent at the mark" describes a sale that cannot happen, and "bankrupt at the mark" describes a loss nobody has agreed to take. A bucket deep past its own bankruptcy price over a book that is filling happily does not need deleveraging — it is being liquidated, which is the system working. A bucket that is still technically solvent over a book that will not touch it does, because nothing else will close it.
So Monaco deleverages the position the book refused, whether or not it is bankrupt. **ADL can therefore reach a solvent bucket** — and when it does, the counterparty is *paid* for the fill rather than charged, because a solvent bucket's pinned price sits on the profitable side of the mark for whoever takes the other side of it.
### How much the venue will pay before handing over
The book "refusing" a position means refusing it at a price, and the venue's liquidation mode sets that price. There are two modes:
| mode | meaning |
| - | - |
| **Zero** | fill at the pinned price or not at all. The insurance fund is never charged for execution; anything the book declines goes to ADL on the **first** refusal — below three quarters of maintenance there is nothing to buy by waiting, and every additional attempt is time the counterparties are exposed to. |
| **Unbounded** (default) | fill at whatever the book charges. The fund absorbs the difference without limit. Because this mode sets no price for the book to fail to meet, the only evidence of refusal is the book having no depth for the position across consecutive attempts — about a minute of them, or fewer (never just one) when the bucket's shortfall would consume more than half of what the insurance fund has left. |
When the insurance fund is empty the venue switches to **Zero** automatically and stays there until it is switched back once the fund is replenished: a fund with nothing left cannot pay for fills past the pinned price, so deleveraging takes over instead.
### There is no minimum event size
Monaco does **not** impose a floor below which ADL cannot happen. A small refused position is deleveraged like a large one. A floor would let small holes accumulate as uncovered bad debt with no mechanism to clear them; Monaco would rather deleverage a small position than quietly socialize an unbounded number of small losses.
## The pinned price
The pinned price is where the position's remaining collateral is exactly consumed — the price at which the bucket's equity reaches zero. It is derived from the position's entry and the collateral behind it, **not** from the mark.
It is fixed **once**, when the bucket enters liquidation, and does not move again. That is worth stating carefully, because it is the property everything else rests on:
* Every step along the way is priced against the same number: the protected slices' limits, the terminal attempt's limit in the Zero mode, and every ADL chunk's fill price. The price does not drift as the position is worked down, so a counterparty reached in the last chunk gets the price a counterparty reached in the first one got.
* The mark may travel arbitrarily far in the meantime and the pinned price does not follow it. This is not an approximation: the price is derived from entry and collateral, quantities the mark does not enter, so re-deriving it after a mark move returns the same answer. Pinning it simply removes the chance to re-derive it from state that has since changed.
Every ADL fill is at the pinned price. It is rounded onto the market's price grid **away** from the mark, so the rounding leaves a fraction of a tick with the liquidating bucket rather than adding to what the fund carries.
### Cross-margin buckets
A cross-margin bucket holds positions in several markets against one pool of collateral, so it has no single closing price — there are many combinations of prices at which its equity would be zero. Monaco picks the one that moves **every market the same fraction of its own mark**, in the direction that recovers the bucket's shortfall, scaled by how much margin each market requires.
So each market absorbs the shortfall in proportion to the risk it was carrying, not to how many units of it you happened to hold — two positions of equal size and equal risk contribute equally whether the asset trades at $1 or $50,000. That includes markets where the bucket was in profit: the bucket's collateral and the profit realized on its winning legs flow out through its losing legs, and what neither covers is shared across every counterparty. One market is closed per episode; each follow-on episode re-prices what remains from fresh state and reaches the same result.
## Who is selected, and at what price
The pool is every risk bucket holding the opposite side of the refused position in that market — isolated and cross alike. Only the risk bucket being liquidated is excluded, along with buckets already frozen in bad debt. The same margin account's other risk buckets are ordinary counterparties: if you are long in a cross bucket and short the same market in an isolated one, your own hedge can take the fill, with nothing leaving your account.
An account that is itself being liquidated **is** eligible. When two accounts on opposite sides of the same market are both failing, closing them against each other is the only disposal that touches nobody solvent — and excluding them would leave both stuck, since neither could reach the other.
Every counterparty in an episode fills at the **same** pinned price, and nothing is capped: once you are selected for a quantity, you fill that quantity at that price. What differs is who is reached first.
The cost of a fill is the same for everyone: the gap between the pinned price and the mark, per unit. That gap can be *negative* — when the liquidating bucket is still solvent its pinned price sits on the profitable side of the mark, and the fill pays you. Your entry only decides which pocket a cost comes from: a trader who entered beyond the pinned price gives up unrealized profit they had not banked; a trader who entered inside it pays from posted collateral. That gap can matter more than it looks for a cross-margin account: a leg's unrealized profit may be what is carrying a losing position in another market, so a fill that "pays" you can still leave your whole account short. Monaco therefore selects by **where the fill leaves your whole account**, in two stages, exhausting the first before touching the second.
What counts as "your account" here is deliberately conservative. Monaco does not assume you will still be holding the collateral you hold right now — you are free to withdraw it, and the plan is made before the fill lands. It counts only what you could not take out if you tried: your initial margin, plus any unrealized gain across your positions, since the withdrawal gate never lets an unrealized gain fund a withdrawal. Collateral above that is yours to move and is not counted in your favour. Nor is margin you no longer have: if a position moved against you, what you actually hold is what counts.
### 1. Traders the fill leaves covered
Everyone whose account would still be at or above zero on those reserves after closing the whole position at the pinned price. This group is filled **pro-rata by position size, with no cap** — being in it already means the whole position fits — so the cost spreads across the market in proportion to exposure. As long as this group can absorb the refused position, nobody else is touched at all.
A fill here can still leave your account under its maintenance requirement, in which case it enters the ordinary liquidation flow afterwards with equity still to liquidate. Selection protects your solvency, not your margin ratio.
Being in profit is not what puts you in this group, and being at a loss is not what keeps you out. A loss your own initial margin covers is what that margin was posted for. Monaco does not rank counterparties by how profitable they are.
### 2. Traders the fill leaves past zero
Reached only when the first stage has no position left to give. These accounts become bankrupt themselves, and the same machinery that reached them runs for them: their remaining positions close at their own pinned price against their own markets' opposite sides. The loss travels until it lands on accounts that can carry it.
This stage closes a position **in full** rather than splitting it, and reaches the account it would leave closest to zero first. A partial close here would leave the loss sitting on a live position that has to be dealt with again later; closing it out settles it at a price already known.
### What reaches the fund, stated plainly
The liquidated trader lands on exactly zero, always — the pinned price is by construction the price at which its equity reaches zero. A counterparty pushed past zero with positions still open carries the loss onward through its own episode. The only thing an episode charges to the insurance fund is a counterparty left **flat and negative** — no position left to carry the loss with — and it is charged by exactly that shortfall, recorded on the fill. Monaco meters it per episode, so an episode that consumed the fund is never indistinguishable from one that did not.
Because perpetual bookkeeping is zero-sum, the opposite side always holds enough quantity to close the refused position in full. Every episode closes the position completely, with no human in the loop.
## Slice sizes
ADL has no minimum slice size. A market fragmented into many small holdings must still be closeable, so no holding is too small to be reached.
Partial slices are sized on the market's lot step. Lots are allocated by largest remainder, and never to a holder outside the group currently being reached. A counterparty whose position is closed in full is closed at its exact size, on the lot step or not. The last part of the position being closed is always placed, even when it is smaller than one lot, so ADL never strands a sub-lot remainder.
### An ADL fill can leave your position off the lot step
The position being closed may itself be off the market's lot step. When the last counterparty reached takes only part of its position, that remainder lands on them. For example, a holder of 10 on a step of 1 who absorbs the final 0.5 is left holding 9.5, a size an ordinary order on that market cannot trade down to.
You can always close it. A **reduce-only order for exactly your full position size** is accepted even when that size is off the lot step or below the minimum order size. A TP/SL set to close the position sizes itself to the live position in the same way. Only a partial reduce-only order has to sit on the lot step.
## Your orders after an ADL fill
When ADL closes your position in full, your resting **reduce-only** orders on that market in that risk bucket are cancelled with it — there is no position left for them to reduce, and leaving them would let them fill against a position you open later. They end `CANCELLED` with the terminal reason `AUTO_DELEVERAGING`, not `LIQUIDATION`: you were deleveraged as a counterparty, not liquidated. Your other resting orders stay on the book. A risk bucket the fill left under its maintenance margin enters the ordinary liquidation flow, which cancels its resting orders as liquidation always does.
## Episodes, and why you are told afterward
An ADL event is an **episode**. The pool, the price, and every slice are fixed at the moment the episode opens. Execution then proceeds in chunks that carry out that fixed plan — they never recompute it. Episodes run one at a time per bucket; a bucket with an episode already open is never re-planned underneath it.
If a selected counterparty's position has shrunk below their planned slice by the time their chunk executes, their slice is **clamped to whatever they actually still hold**. Nobody is ever over-closed, and the shortfall is not redistributed onto the other counterparties in the plan — it rolls into a follow-on episode that re-plans against fresh state. The price never changes: every fill is at the pinned price, which was fixed before the episode even opened, and where that leaves each counterparty is re-checked from their account **as it stands at that moment**.
Every chunk re-checks that the bucket is still below three quarters of its maintenance margin. A bucket carried back above that line between open and chunk — by a mark rebound, a funding payment, a deposit — is handed back to the ordinary liquidation step rather than closed against counterparties it no longer needs.
Only something *external* can carry it back. The episode's own fills cannot: closing part of a position at its pinned price leaves the bucket exactly as far from that line as it was before, in proportion. So an ADL chain never accelerates itself into deleveraging more than it had to, and never stalls itself part-way through either.
**Notifications fire when an episode closes, not when it opens.** This is intentional: Monaco does not announce the list of who is about to be closed before executing it.
Be clear about what that does and does not mean. It is not a guarantee that an in-flight episode is unobservable. A counterparty filled in an early chunk sees that fill in their own position and balance straight away, well before the episode closes. And ADL fills print as trades — they appear on the public trades endpoint and are folded into candles, volume and last-price like any other trade. Anyone watching the tape can see an episode in progress.
## Known gaps
Recorded here rather than discovered later:
* **Statements do not yet label ADL fills.** An ADL fill appears as an ordinary fill.
* **Charts and share cards are not yet ADL-aware.**
* **Public trade data does not label ADL fills either.** To an outside observer they are indistinguishable from ordinary trades — visible, but not interpretable.
All three matter for the same reason. ADL prints off-market by construction — the pinned price is away from where the book last traded — and an unlabeled off-mark print is genuinely misleading. These are tracked as known debt rather than quietly deferred.
## Summary
* ADL is tier 3, and runs when the **book refuses** a position that is below three quarters of its maintenance margin — not when the bucket is bankrupt. A solvent bucket the book will not take is deleveraged; a bankrupt one the book is filling is not. No minimum size.
* Every fill is at the position's **pinned** price, derived from entry and collateral and fixed when liquidation began, never the mark. A cross-margin bucket moves every market the same fraction of its mark, weighted by the margin that market requires.
* How far past that price the venue will pay the book before handing over is the venue's liquidation mode: Unbounded by default, and Zero — where the fund is never charged for execution — once the insurance fund is empty.
* Counterparties are reached by where the fill leaves their whole account: everyone it leaves at or above zero first, pro-rata by size; then, only if needed, those it pushes past zero, closest to zero first and closed in full. Nothing is capped once selected. Your own other risk buckets are eligible counterparties; only the liquidated bucket is excluded.
* A counterparty pushed past zero carries the loss on through its own episode; only one left flat and negative reaches the insurance fund, by a recorded shortfall.
* Plans are fixed at episode open; nobody is over-closed; a bucket carried back above three quarters of maintenance by anything external is handed back to the book; you are notified at close, though the fills themselves print publicly as they happen.
# Funding Rates
Source: https://docs.0xmonaco.com/trading-mechanics/funding-rates
Understand when you pay funding, when you receive it, and how it is calculated.
Funding is a recurring payment between long and short position holders. It encourages perpetual prices to stay close to the underlying index, despite having no expiry date.
## At A Glance
| Rule | Monaco behavior |
| - | - |
| Settlement | Fixed 60-minute windows, aligned to hour boundaries. |
| Positive funding | Longs pay shorts. |
| Negative funding | Shorts pay longs. |
| Who participates | Positions open at the settlement timestamp. |
| Payment value | Funding rate × position size × mark price. |
| Protocol share | Monaco takes no part of funding payments. |
Close a position before settlement and it does not pay or receive funding for that window.
## What The Payment Means
Illustrative calculation\$10,000 × 0.01% = \$1For a position worth \$10,000 at the mark, a positive 0.01% settlement rate means a long pays \$1 and a short receives \$1. The position must be open at settlement.
The displayed implied rate is an estimate for the current window. It refreshes with each accepted premium sample, rather than as a continuous real-time stream.
## How The Rate Is Set
Monaco measures the premium or discount using **impact prices**: the average execution prices for buying or selling a specified quote notional through the order book. The input is not the last trade, the best bid/ask, the arithmetic midpoint, or trade VWAP.
Each accepted sample is first bounded to the maximum funding rate. The bounded samples are then averaged with equal weight across the window, so a sample near settlement counts no more than one at the start. The interest-rate, or carry, component is **zero**. Funding follows the signed premium or discount directly, subject to a cap.
The cap is a single platform-wide setting, **0.75% per settlement by default**, applied to every market. It is not derived from a market’s margin ladder.
## Low-Sample Windows And Downtime
If too few samples are accepted, the window uses the last settled funding rate. A market with no previous settled rate uses zero. Settlement records include a flag identifying carried rates, along with timestamps, sample counts, and the mark price used for payment.
The next funding time is the scheduled boundary. Settlement occurs at or shortly after it while the market is alive. Boundaries that pass during downtime are skipped, not settled retroactively.
## Rates And History
Use the [Markets reference](/developers/markets) for public funding state and history, and [Positions](/developers/positions) for position risk. The full calculation reference retains the impact-price formula, per-sample bounds, uniform weighting, cap, and settlement fields.
The funding mechanism is a recurring payment that keeps a perpetual futures market (perp) anchored to its index price. In traditional markets, a dated futures contract (with a fixed term) converges to the underlying spot price at expiry, as arbitrageurs are incentivized to do so. A perp has no fixed expiry, and so, funding rates enable this convergence through payments between long and short position holders.
Funding Mechanics at a Glance
* Funding windows are fixed 60-minute windows aligned to hour boundaries, with payments settled at each boundary
* The interest-rate (carry) component $I$ is set to 0% (as opposed to the legacy 1bp/8h convention).
* Impact Bid and Impact Ask prices are sampled throughout the funding window and used to compute the signed funding premium/discount, which is aggregated over the window.
* If funding is positive, the perp is at a premium to the index, and longs pay shorts. If funding is negative, the perp is at a discount, and shorts pay longs.
* Funding is exchanged directly between position holders. Monaco does not collect any portion of funding payments.
This creates an incentive for traders, arbitrageurs, and market makers to bring the perp price back toward the index.
Funding Formula
For market $m$, a funding window ending at time $t$ is computed as:
$$
F_m(t) = \operatorname{cap}(\bar{P}_m(t), -F_{\max,m}, F_{\max,m})
$$
Where:
* $F_m(t)$ is the final funding rate applied at settlement
* $\bar{P}_m(t)$ is the weighted average signed premium or discount over the funding window, computed from samples that are each individually bounded to $\pm F_{\max,m}$ (see [Window Aggregation](#window-aggregation)).
* $F_{\max,m}$ is the absolute maximum funding rate — a single platform-wide configured bound (0.75% per settlement by default), applied identically to every market
* $\mathrm{cap(x, a, b)}$ means $\min(\max(x, a), b)$, with $a < b$
The sign convention is:
* $F_m(t) > 0$: longs pay shorts.
* $F_m(t) < 0$: shorts pay longs.
The cap is a risk bound, not a pricing rule. Inside the cap, every change in the signed premium/discount passes directly into the funding rate. $F_{\max, m}$ is a single platform-wide operational parameter — it is not derived from any market's margin schedule, and every market shares the same bound.
Interest Rate (Carry)
The protocol's funding mechanism sets the interest-rate, or carry, component to zero:
$$
I_m(t) = 0
$$
Funding is driven purely by the observed premium/discount, rather than a fixed interest-rate assumption.
Legacy funding formulas often combine premium/discount with an interest-rate anchor, such as:
$$
F = P + \mathrm{clamp}(I - P, -0.05\%, 0.05\%)
$$
When $(I - P)$ sits inside that clamp band, the final funding rate is pinned to $I$: changes in the premium/discount have no effect exactly where the signal is most informative. Monaco avoids that dead zone by using the signed premium/discount directly and setting carry to zero. This does not mean carry is theoretically irrelevant; it means the mechanism avoids importing a stale universal carry constant.
Impact Prices
Monaco does not use the last traded price, best bid/ask, arithmetic mid-price, or trade VWAP as the premium input. Those values can be stale or easy to distort. Funding should not disappear because a market has no recent trades, nor should it be dominated by a few small prints. Instead, the mechanism asks: if someone tried to buy or sell a specified quote notional $(Q_m)$ at a given moment, what average execution price would they get?
That produces two "impact prices":
* **ImpactAsk:** the average execution price to buy $Q_m$ through the ask side of the order book
* **ImpactBid:** the average execution price to sell $Q_m$ through the bid side of the order book
The relevant question is where meaningful exposure can be executed relative to the index price. Impact prices answer that from the order book, and are averaged over time.
Premium/Discount Sample
At each accepted sampling time $i$, a signed premium/discount sample is computed:
$$
P_i =
\frac{
\max(0, B_i^{impact} - S_i)
-
\max(0, S_i - A_i^{impact})
}{
S_i
}
$$
Where:
* $S_i$ is the index price.
* $B_i^{impact}$ is the impact bid price.
* $A_i^{impact}$ is the impact ask price.
In a well-functioning order book, $\mathrm{ImpactBid} \leq \mathrm{best\ bid} < \mathrm{best\ ask} \leq \mathrm{ImpactAsk}$ for any quote notional $Q_m$. The sample is divided by the index price, so the result is dimensionless and comparable across markets with different price levels.
Window Aggregation
Every accepted sample is first bounded to the same maximum funding rate that bounds the settled rate:
$$
\tilde{P}_i = \operatorname{cap}(P_i, -F_{\max,m}, F_{\max,m})
$$
The impact prices behind a sample are read from live resting orders, which can be placed and cancelled at will. Bounding each sample means no single sample — and so no brief burst of resting depth — can contribute more than one window's worth of the maximum rate to the average. A book that genuinely sustains a premium beyond $F_{\max,m}$ still settles at the cap, because every one of its samples contributes the cap.
The bounded samples are then aggregated over the funding window as a uniformly weighted time average:
$$
\bar{P}_m(t) =
\frac{1}{N}\sum_{i=0}^{N-1} \tilde{P}_i
$$
Every accepted sample in the window counts the same, whenever it arrived. A window is a long sequence of samples (720 of them per hour at the default cadence), so an equal weighting both smooths short-lived order book noise and makes the whole window equally expensive to influence — a burst of resting depth placed just before settlement counts no more than the same burst placed at the top of the hour. If a window contains fewer accepted samples than the configured minimum (`FUNDING_MIN_SAMPLES`), the window settles at the last settled funding rate; a market with no previously settled rate settles at zero.
Settlement
At the end of each window, Monaco applies the funding rate. The payment for a position is:
$$
\mathrm{Funding\ Payment} = F_m(t) \times \mathrm{Position\ Size} \times \mathrm{Mark\ Price}
$$
Positive funding means longs pay shorts; negative funding means shorts pay longs. Only positions open at the settlement timestamp pay or receive funding; a position closed before settlement is not charged for that window.
Alongside each settlement, Monaco publishes the final rate $F_m(t)$, the per-unit funding delta it produced, the window's timestamps and accepted-sample count, the reference price (the mark price used for payments), and a `carried` flag marking windows that settled at the carried last rate under the minimum-sample rule, so the rate is fully auditable. The current implied rate for the in-progress window is an estimate refreshed on each accepted premium sample — roughly every sampling interval — rather than a continuous real-time stream. Likewise, `nextFundingTime` is the scheduled window boundary, not a guarantee: settlement lands at or shortly after it while the market is alive, and boundaries that pass during downtime are skipped rather than settled retroactively.
For integration details, use [Markets](/developers/markets) to query public funding state and funding history, and [Positions](/developers/positions) to monitor your position risk.
# Liquidations
Source: https://docs.0xmonaco.com/trading-mechanics/liquidations
How Monaco liquidates under-margined risk buckets: the two rungs of the ladder, the price each is limited at, how the queue is ordered, when the book stops owning the position, and what happens to bad debt.
A perpetual exchange must liquidate under-margined accounts before their losses become systemic. Monaco does this in two rungs, and what separates them is **what the closing order is priced against**.
* **Protected liquidation** protects price quality. It reduces risk with bounded, reduce-only execution while the bucket still has room to spare.
* **Terminal liquidation** protects solvency. Once a bucket falls below three quarters of its maintenance margin that room is gone, and the objective changes: closing the position matters more than preserving what is left of its equity.
Both rungs send reduce-only immediate-or-cancel (IOC) orders. The protected rung always carries an explicit limit; the terminal rung's limit is set by the venue's [liquidation mode](#how-far-past-the-pin-the-venue-will-pay).
## The pinned price
When a risk bucket enters liquidation, Monaco fixes a closing price for each of its positions: the price at which that bucket's equity would reach exactly zero. This is the **pinned price**, and it does not move again for the life of the liquidation.
It is derived from entry and the collateral behind the position, and the mark does not enter the derivation at all. That is what makes pinning safe rather than approximate — re-deriving it after a mark move returns the same number, so fixing it once only removes the chance to re-derive it from state that has since changed.
Every limit below is priced against it, and every [auto-deleveraging](/trading-mechanics/auto-deleveraging) fill settles at it.
## The two rungs
Let `E` be bucket equity and `M` the maintenance margin required, priced off the market's margin ladder — `M` is the ladder's piecewise `MM(N)` per bracket, not a single flat rate.
$$
\text{Healthy} \iff E \geq M
$$
$$
\text{Protected} \iff \tfrac{3}{4}M \leq E < M
$$
$$
\text{Terminal} \iff E < \tfrac{3}{4}M
$$
Liquidation begins as soon as equity falls below maintenance. The terminal rung begins only once the bucket is materially deeper underwater.
The three-quarters line has a precise meaning rather than being a tuned constant. The distance between a position's mark and its pinned price is exactly the maintenance margin that position is carrying — so the fraction of that distance still unspent *is* the fraction of maintenance margin the bucket still holds. At `E = ¾M` the mark has travelled a quarter of the way to the pin. The line is a position in that corridor, and it is the same fraction in every market the bucket holds, at the same moment.
### Protected rung
Above the line, a slice is limited at the position's **protected rung** — three quarters of the way from the pinned price back toward solvency. The limit therefore tightens toward the pin as equity decays, and a fill can never cross into the bucket's own bankruptcy.
Sizing is minimal: the slice closes enough to restore the bucket to its maintenance requirement plus a buffer, not the whole position. One position is sliced per step, chosen by which slice buys the most risk reduction per unit of expected execution cost. Slices are additionally capped by remaining position size, a maximum slice notional, and the executable book depth within the limit price.
### Terminal rung
Below the line, price protection has already failed its purpose. The slice requests the **full remaining position**, with no notional cap — every further tick of delay below three quarters of maintenance is insurance-fund money. Where its limit sits depends on the venue's liquidation mode, below.
Whatever the book will not take is escalated to [Auto-Deleveraging](/trading-mechanics/auto-deleveraging), which closes it against traders holding the opposite side at the pinned price.
### How far past the pin the venue will pay
The venue runs one of two liquidation modes. There is no middle setting.
* **Unbounded** (the default). The terminal slice carries no limit at all: the book takes the position at whatever it charges, and the insurance fund absorbs any fill past the pinned price. Deleveraging is reached only once the book has had no depth for the position across consecutive attempts for about a minute — sooner, though never on a single attempt, when the bucket's shortfall would consume more than half of what the insurance fund has left.
* **Zero**. The terminal slice is limited at the pinned price itself, so no book fill can create bad debt: the fund pays nothing for execution, and anything the book declines at that price is handed to deleveraging on the first refusal.
The choice is a policy dial, not a safety mechanism: it trades fund draw against how many counterparties an event reaches. When the insurance fund is empty, the venue switches to **Zero** automatically and stays there until it is switched back once the fund is replenished — a fund with nothing left cannot pay for fills past the pin.
## Queue priority
Pending liquidations are not processed first-in-first-out. Monaco orders them by **absolute breach depth**:
$$
D = \max(M - E,\ 0)
$$
The deepest deficit is served first, with arrival time and then bucket identity as tie-breakers. Age prevents similarly-placed buckets from being reordered forever, and priority is recomputed on every requeue, so a bucket that deteriorates between attempts moves up.
Absolute depth rather than a ratio is deliberate. A small account with an extreme relative shortfall is less dangerous to the system than a large account with a milder ratio but a far larger dollar hole — and it is the dollars that reach the insurance fund.
## Post-fill revalidation
The ladder never blindly continues to a worse price. After every attempt Monaco recomputes equity, the maintenance requirement and remaining exposure, then acts on the result:
* `E' >= M'` — liquidation stops and the bucket returns to normal.
* Exposure fully closed and `E' < 0` — liquidation stops and the deficit is crystallized as [bad debt](#bad-debt-and-the-insurance-fund).
* `¾M' <= E' < M'` — the bucket leaves the terminal rung but stays in liquidation.
* `E' < ¾M'` — the bucket stays on the terminal rung and is requeued with updated priority.
A bucket cannot cross back above the three-quarters line through its own fills. Closing part of a position at its pinned price leaves the bucket exactly as far from that line as it was, in proportion — so only something external recovers it: a mark rebound, a funding payment, a deposit. That is what stops a liquidation from either accelerating itself or stalling part-way through.
## Bankruptcy price and bad debt
For a single position with signed quantity `q` (positive for long) and mark price `S`, bucket equity after a full close at price `p` is:
$$
E'(p) = E + q\,(p - S)
$$
The pinned price is the closeout price that makes post-close equity exactly zero:
$$
p_{\text{bk}} = S - \frac{E}{q}
$$
For a long, execution below `p_bk` creates bad debt; for a short, execution above it does. The design rules are:
* Protected liquidation cannot cross bankruptcy — its limit sits strictly inside the pin.
* Terminal liquidation crosses it only in the Unbounded mode, where the insurance fund absorbs the difference. In the Zero mode, not at all.
* Bad debt is crystallized only after exposure is closed, or after an explicit terminal closeout policy.
* Insurance resolution is downstream of crystallized bad debt. It is not a substitute for liquidation execution.
### Bad debt and the insurance fund
A risk bucket that ends flat with negative equity is in **bad debt**. The margin account has lost exactly the collateral allocated to that bucket and no more; the rest of the shortfall is the insurance fund's. This holds for cross and isolated risk buckets alike, and it applies however the bucket became flat and insolvent — through liquidation, through auto-deleveraging, or through your own closing trade.
While a bucket is in bad debt it is frozen:
* Its allocation cannot change in either direction. Transfers into or out of it are refused; a top-up from the margin account does not release it.
* The insurance fund pays the shortfall as it can, and each payment is credited to the bucket as it is made, so the bucket can be paid in parts. Once the shortfall is fully covered the bucket is settled flat: an isolated risk bucket closes, and a cross risk bucket returns to normal, empty.
* If the insurance fund is empty, the bucket stays frozen until the fund can pay. Bad debt is never written off by hand; the fund is the only way out.
## Liquidation fee
A liquidation slice is charged a fee derived from the maintenance margin it releases, evaluated at the step mark rather than at the fill price:
$$
\text{fee}_{\text{uncapped}} = k\,\Delta M = k\,\bigl(M(N_{\text{before}}) - M(N_{\text{after}})\bigr)
$$
`M(N)` is the ladder's piecewise maintenance-margin function, so `\Delta M` is a bracket difference rather than a flat rate times closed notional: a slice that closes a position across a tier boundary releases the whole marginal step, not just the tier-1 amount.
The release fraction `k` is one half. That choice is what keeps a clip a survival path: with the mark unchanged and execution at the mark, a slice moves the bucket's health `E - M` by `(1 - k)\,\Delta M`, so at `k = ½` half the released maintenance margin is the fee and half is headroom the bucket keeps. At `k = 1` the fee would consume the entire release and a clip could never restore health on its own. Slippage, a mark move, fee clamping, or rounding change the realized figure.
The charged fee is capped by what the bucket still has — remaining cash and mark-to-market equity — and rounded down when a fee unit is configured. It is a junior claim: it never creates or deepens bad debt. What is collected is credited to the insurance fund.
## Risk controls
Liquidation is aggressive on the terminal rung, but bounded:
* Orders are internal, liquidation-originated, and reduce-only.
* Orders are IOC and never rest on the book.
* Price limits are derived from the bucket's own pinned price rather than a flat band around the mark: the protected rung always, and the terminal rung in the Zero mode.
* The mechanism requires a valid mark price and a tradable market state.
* Before slicing, the liquidated bucket's own resting orders are cancelled. Forced closes do not apply [self-trade prevention](/trading-mechanics/self-trade-prevention): liquidation slices fill against resting orders from your other risk buckets and sub-accounts in price order, exactly as against anyone else's, and count that depth as available; [auto-deleveraging](/trading-mechanics/auto-deleveraging) fills no resting orders and can close an opposing position your other risk buckets hold; a position it fully closes has its risk bucket's resting reduce-only orders on that pair cancelled with `AUTO_DELEVERAGING`.
* Every step publishes enough audit data to reconstruct queue priority, breach depth, the limit price used, attempted and filled quantity, and post-fill equity and maintenance requirement.
## Summary
For each liquidation worker step:
1. Refresh bucket equity `E` and maintenance requirement `M`.
2. Select the highest-priority queued bucket by breach depth `D`, with age and identity as tie-breakers.
3. If `E >= M`, resolve the candidate.
4. If `E >= ¾M`, slice one position, sized to restore health, limited at its protected rung.
5. If `E < ¾M`, request the full remaining position — limited at its pinned price in the Zero mode, with no limit in the Unbounded mode.
6. Apply fills and recompute equity, maintenance requirement and remaining exposure.
7. Stop if the bucket recovered or is flat; otherwise requeue with updated priority.
8. If the book refused the position on the terminal rung, hand it to auto-deleveraging.
The key principle: **protected liquidation preserves execution quality while the bucket can still afford it; terminal liquidation closes the position at a price fixed before the trouble started.**
When the book will not meet that price, the position leaves the book entirely — see [Auto-Deleveraging](/trading-mechanics/auto-deleveraging). Note that the escalation is driven by the book **refusing to fill**, not by the bucket going bankrupt: a solvent bucket the book will not take is deleveraged, and a bankrupt one the book is still filling is not.
For monitoring position risk and liquidation distance on your own account, see [Margin](/trading-mechanics/perps) and [Positions](/developers/positions).
# Margin
Source: https://docs.0xmonaco.com/trading-mechanics/perps
Understand the collateral supporting your perpetual positions.
Perpetual futures let you take price exposure without owning the underlying asset. Each position requires collateral in your margin account. Fills update your position, margin accounting, unrealized PnL, and funding history.
## The Numbers To Understand
| Term | Meaning |
| - | - |
| Account equity | Collateral plus realized and unrealized PnL on your futures positions. |
| Free collateral | Account equity less the margin required to open positions. |
| Initial margin | The requirement to open exposure, accounting for selected leverage and the market’s margin ladder. |
| Maintenance margin | The minimum required to keep a position open. |
| Margin ratio | Maintenance margin divided by equity. A higher ratio means greater proximity to liquidation. |
The **mark price** is used for margin requirements, PnL, and liquidations. It is anchored to the oracle and blends the book’s basis over approximately **150 seconds**, rather than responding to individual quotes or cancels. The **index price** is the fair-value reference used for funding.
## How Margin Is Applied
Every position lives in a risk bucket, cross or isolated, and each bucket is contained at the collateral allocated to it: the margin account can lose no more than that allocation through it. See [What a risk bucket can lose](#what-a-risk-bucket-can-lose). The [Accounts page](/accounts) describes how the selected mode affects locked USDC collateral.
Portfolio-aware margining for correlated positions is on the roadmap. It is intended to recognize offsetting risk, such as long BTC against short ETH.
### Selected Leverage And Margin Tiers
Initial margin is the greater of the selected-leverage requirement, scaled to the current mark, and the market’s ladder floor. Maintenance margin also follows the ladder. Deductions keep both requirements continuous at tier boundaries.
Example\$100,000 at 10× leverageAt entry, this position requires \$10,000 of initial margin, even if the first tier’s ladder floor is only \$2,000.
If an order would move the position above tier 1 and its requested leverage exceeds the new tier’s maximum, it is rejected. Lower the leverage and resubmit. Current market configuration is authoritative; the launch ladder is a snapshot.
## Position And Exit Controls
Monaco uses a single net position per market, with no hedge mode. **Reduce-only** prevents a close order from increasing or reversing exposure.
TP/SL orders can attach to positions or entry orders. A trailing stop follows the mark’s best level by a set distance. TP/SL triggers use the mark, while execution occurs against the book: **trigger price and fill price are different**. Market legs can pay spread and price impact; limit legs may rest or fill partially.
Standard perpetual market orders use a **10%** protective band. Triggered TP/SL market legs use a **12%** band to increase the chance of reducing exposure. A take-profit can still close at a loss after spread, impact, and round-trip taker fees.
## Review Collateral Before Trading
A risk preview reports acceptance, projected margin, free collateral, estimated liquidation price, and simulated fills. For open positions, monitor the mark, margin ratio, PnL, and liquidation distance.
When an order needs more initial margin than its risk bucket has free, admission draws exactly the shortfall from the parent margin account's unallocated collateral, subject to risk checks. A crossing order is charged its initial margin at the mark, the loss each fill takes against the mark, and its taker fee; a resting order is charged its initial margin at its limit, its maker fee, and any loss it would take against the live mark. Only the part that opens or flips a position is charged, so a close is never refused for its loss. A fill never draws more: it keeps in the risk bucket only what its new position needs and returns the rest; a cancel or released remainder returns everything that order drew, in cross and isolated risk buckets alike. Collateral you allocated yourself is never returned unasked. A position's loss never stops a new order the parent can fund and is never drawn for: a risk bucket below its initial margin takes new orders but stays below it until you add collateral or reduce.
Where automatic refunds are enabled for a market order, unused automatically allocated collateral returns after any positive fill. Requirements, fees, funding, losses, and other orders can keep the final allocation above notional divided by leverage.
The full rules retain refund limits, zero-fill handling, tier formulas, rounding, the launch ladder, and the exact API fields.
Perpetual futures (perps) allow traders to take directional price exposure without owning or exchanging the underlying asset. On Monaco, each perp position requires users depositing collateral in their margin account. Order fills update margin accounting, position state, unrealized PnL, and funding history.
Perps-Specific Controls
Perps introduce controls that do not apply to spot trading:
* **One-way direction:** each market holds a single net position — `side` sets the direction, and `leverage` is required to open. There is no hedge mode; omit the deprecated `positionSide` compatibility field.
* **Reduce-only:** prevents a close order from increasing or flipping exposure.
* **Leverage:** controls notional exposure relative to collateral.
* **Margin:** collateral committed to support open positions.
* **TP/SL:** take-profit and stop-loss orders for planned exits, attached either to an open position or to a margin entry order, whose legs arm once that order creates **or extends** a position — see [Conditional Perps Orders](/developers/trade#conditional-perps-orders). A trailing stop does the same with a trigger that follows the mark's best level by a set distance — see [Trailing Stop](/developers/trade#trailing-stop).
Use reduce-only orders when closing exposure, and see [Order Management](/developers/trade#conditional-perps-orders) for conditional exit orders.
## Perp price band
Every perp order that takes liquidity — market or limit, any time in force — trades only inside a band around the market's **mark price**. With mark `m` and band `k` = 1,000 bps (10%), the band runs from `m / (1 + k)` to `m / (1 - k)`: about 9.1% below the mark to 11.1% above it. A fill at either edge is exactly 10% of its own notional away from the mark, so a buyer and a seller of the same size at the same price are treated alike. Triggered TP/SL legs use a wider 1,200 bps band, so a firing stop trades price quality for a higher chance of reducing the position. These are the defaults. A perp market can be configured with its own band, from 100 to 2,500 bps (1% to 25%), which then replaces both: placements and triggered legs on that market use the same `k`.
The order walks the book in price priority and **stops at the first resting order outside the band**, on either side. That resting order stays on the book; it cannot trade until the mark comes back near it or its owner moves it.
* Whatever filled inside the band settles. A market, `IOC` or `FOK` remainder is cancelled as usual (`FOK` rejects when the band leaves it short), with `terminalReason` `PRICE_BAND`, or `SLIPPAGE_TOLERANCE` when your own `slippageToleranceBps` was the tighter bound.
* A limit remainder rests at its own price only if resting would not cross the book. If the band stopped the order at a resting order its own price crosses, the remainder is cancelled instead, with `terminalReason` `PRICE_BAND`: a resting bid at or above a resting ask would cross the book.
* An order the band keeps from filling anything is rejected as insufficient liquidity before it is accepted.
* A limit order that does not cross the book rests normally, even when it is priced outside the band.
* A perp order on a market with no mark price is rejected as unavailable; the book is never used in place of the mark.
Post-only orders never take liquidity and are not banded. Liquidation and ADL executions are exempt. A client `slippageToleranceBps` can only tighten a market order's band.
A TP/SL **triggers on the mark price and fills against the book**, so its trigger price and its fill price are independent numbers. The mark is an oracle-anchored reference price rather than the book's midpoint: the book's premium or discount to the oracle enters it, and leaves it, only through a \~150 s time-weighted average, so neither a single quote nor a single cancel can move it, and it is sampled on a tick. A market leg then pays the spread it crosses plus the depth its size consumes — usually worse than the trigger, though nothing guarantees a minimum gap. A limit leg may rest or fill only partly. A triggered conditional order reports what its close actually did under `triggeredOrder`, and the close carries `conditionalOrderId` back to the trigger — see [Trigger Price Is Not Fill Price](/developers/trade#trigger-price-is-not-fill-price). Set a take-profit far enough from entry to clear the spread, your own impact and round-trip taker fees, or it can fire exactly as set and still close at a loss.
When a market order automatically funds a new isolated risk bucket, its initial allocation includes an execution-price allowance and estimated fees. Where automatic execution-collateral refunds are enabled, unused automatic collateral returns to the parent margin account in the same settlement once the order has executed. Any positive fill releases it — a market order that filled only part of its quantity before the remainder was cancelled releases just as a fully filled one does, and the release never exceeds the allocation the order originally drew. This release is limited to collateral automatically allocated for that execution; existing explicit collateral stays in place. The position retains its required margin, paid fees, funding, losses, and any collateral needed by other open orders. Final allocation can therefore exceed order value divided by leverage when the fill opens at a loss against the mark price. Risk previews include the same settlement behavior. An order that executed nothing at all releases through the ordinary cancellation-settlement rules instead.
Admission is the only point where an order draws collateral from the parent margin account. A crossing order is charged its initial margin at the mark, the loss each fill takes against the mark, and its taker fee; a resting order is charged its initial margin at its limit, its maker fee, and any loss it would take against the live mark. Only the part that opens or flips a position is charged. If the risk bucket's free collateral falls short, admission draws exactly the shortfall, bounded by the parent's unallocated capacity and the usual risk checks. A first-use order creates its risk bucket holding only the explicit `riskBucketCollateral` you supplied, or nothing, and admission draws the rest in the same way. A fill does no margin check and draws nothing: of what it releases, it keeps in the risk bucket only what the new position needs and returns the rest to the parent. When an order releases — a cancel, an unfilled `IOC` or partial-fill remainder, a replace to a smaller order — the risk bucket returns to the parent exactly what that order drew and has not used, in `ISOLATED` and `CROSS` risk buckets alike. Collateral you allocated yourself, including `riskBucketCollateral`, is never returned automatically. A position's loss is never drawn for and never refuses an order the parent can fund: the parent pays only for orders, so a risk bucket below its initial margin stays below it until you add collateral or reduce. Risk previews follow the same path.
Risk Preview
Before placing a non-trivial perp order, preview the account impact with `simulateOrderRisk`. A trading UI should show whether the order is accepted, projected margin, free collateral after trade, and estimated liquidation price before the trader submits. Pass the MARKET order's `slippageToleranceBps` so the preview runs under the band the order will, and read `expectedMatchResult` — the engine's simulated fill, in the same shape a placement's `matchResult` carries — for the size's own price impact and any unfilled remainder; see [Pre-trade simulation](/sdk/typescript/margin-accounts#pre-trade-simulation).
For open positions, use `getPositionRisk` or authenticated WebSocket alerts to keep mark price, margin ratio, PnL, and liquidation distance current.
See [Account Vault](/developers/vault#deposit-directly-to-perps-collateral) and [Positions](/developers/positions).
Core Concepts
* **Account Equity:** The collateral in your margin account summed with realized and unrealized PnL on your futures positions.
* **Free Collateral:** Account Equity less the margin required to open positions.
* **Initial Margin:** The position requirement is the greater of its selected-leverage initial margin, scaled to the current mark, and the market's ladder floor: `IM_floor(N) = N x initialMarginRatio - initialMarginDeduction`. See [Margin ladders](#margin-ladders).
* **Maintenance Margin:** Minimum margin required to keep a position open, given by the ladder's maintenance requirement: `MM(N) = N x maintenanceMarginRatio - maintenanceMarginDeduction`.
* **Margin Ratio:** Maintenance Margin divided by Account Equity, i.e. `maintenanceMarginRequired / equity`. Higher means closer to liquidation. At zero or negative equity with a maintenance requirement, the ratio reads as the largest representable value — a maximal-distress sentinel, not a measurement; clamp it for display. With no open positions it reads zero.
* **Index Price:** The market-derived fair-value price referenced for funding calculations.
* **Mark Price:** The price used for margin requirements, PnL calculations, and liquidations. It is anchored to the oracle price and blends the order book's basis to the oracle in — and back out when the book goes away — through a \~150 s time-weighted average, so transient quotes and cancels do not move it.
* **Liquidation Price:** Mark price at which a position or portfolio would be liquidated. Above tier 1, this is solved piecewise across the ladder's brackets, since maintenance margin changes rate at each bound. A blank or absent value means unavailable, never zero.
Margining
### What a risk bucket can lose
Every position lives in a risk bucket, cross or isolated. A bucket is backed by the collateral **allocated** to it, and that allocation is the most its margin account can lose through it:
* **Cross and isolated alike.** The cross bucket is contained at its allocation exactly as an isolated one is. Unallocated collateral in the margin account can fund new orders, but it does not back positions already open, so idle collateral does not keep a cross position open. A cross bucket is liquidated at its own maintenance margin, with no automatic backing from the account.
* **The insurance fund absorbs every loss past the allocation.** If a bucket closes out below zero, the account loses the allocation and the insurance fund pays the rest, whatever else the account holds. The engine's live account equity never counts a bucket's loss past its allocation, open or realized. Persisted reads sum unrealized PnL without that cap: the portfolio margin totals (`sdk.portfolio.getMargin`) and the margin-account summary served when the live engine view is unavailable can show an open loss past the allocation. The bucket stays frozen in bad debt until the fund has covered the shortfall — see [Bad debt and the insurance fund](/trading-mechanics/liquidations#bad-debt-and-the-insurance-fund).
* Allocating more collateral to a bucket is how you give its open positions more room.
* **Releasing collateral is held to initial margin.** Moving collateral out of a risk bucket — including reducing a position's margin — must leave it covering the initial margin of its positions plus the reserve for its resting orders, never just maintenance margin. An unrealized gain never funds a release; realized profit does, so a live bucket's withdrawable collateral includes its realized PnL.
* **A flat, solvent risk bucket folds back.** When a risk bucket's last position closes with nothing resting, its allocation and realized PnL return to the parent margin account: an isolated bucket closes, and the cross bucket stays open with nothing allocated.
The roadmap includes portfolio-aware margining for correlated positions. The goal is to recognize offsetting risk, such as long BTC versus short ETH, so a portfolio can receive a netting benefit instead of requiring collateral as if every leg were independent.
Margin ladders
Each perp market prices margin off an ordered ladder of tiers rather than one flat rate. A tier is `(lowerBoundNotional, maxLeverage)`; from `maxLeverage` the ladder derives, rounded to 8 decimal places:
```
initialMarginRatio = 1 / maxLeverage
maintenanceMarginRatio = 1/2 x initialMarginRatio
```
A position's bracket is the *last* tier whose `lowerBoundNotional` is at or below its notional, `abs(quantity) x mark price`.
Margin is continuous across every bound — there is no cliff at a tier boundary. Each tier beyond the first carries a deduction that cancels the jump its raw rate would otherwise create:
```
deduction_k = deduction_{k-1} + bound_k x (rate_k - rate_{k-1})
```
so the ladder's initial-margin floor and maintenance requirement for a position of notional `N` in bracket `k` are:
```
IM_floor(N) = N x initialMarginRatio_k - initialMarginDeduction_k
MM(N) = N x maintenanceMarginRatio_k - maintenanceMarginDeduction_k
```
and both are continuous at `N = bound_k`.
The actual position initial-margin requirement also accounts for selected leverage. Its stored initial margin is scaled from entry notional to current mark notional, then compared with the ladder floor:
```
entryNotional = abs(quantity) x entryPrice
selectedLeverageIM = round(N x storedInitialMargin / entryNotional, 18)
initialMarginRequired = max(selectedLeverageIM, IM_floor(N))
```
At entry, a $100,000 position opened at 10x therefore requires $10,000 initial margin even when its tier-1 ladder floor is only \$2,000.
**Worked example** (crypto majors): tier 1 is 50x (2% / 1%) up to $100M notional; tier 2 is 25x (4% / 2%) above. `MM($100M)`is $1M under either tier's formula — continuous at the bound.`MM($200M) = $200M x 2% - $1M = $3M`, where the $1M deduction is `\$100M x (2% - 1%)\`.
**Admission rule:** an order whose projected post-fill position notional lands above tier 1 is rejected if the requested leverage exceeds that bracket's `maxLeverage` — lower the leverage and resubmit. The bracket is evaluated on position notional only; resting orders are not counted. Initial margin reserved for an order stays `notional x max(1/leverage, tier-1 initialMarginRatio)`.
**Market caps:** a market can carry two optional USD caps; an uncapped market never refuses. Both are checked at admission only, priced at the **mark**, and reject with `400` / `INVALID_ARGUMENT`:
* **Position cap** (`position cap exceeded: …`): the risk bucket's position plus its resting orders on the new order's side, walked in fill order, may not exceed the cap. Orders on the other side are not counted.
* **Open-interest cap** (`open interest cap reached: …`): the market's gross open interest plus the order's opening quantity may not exceed the cap. The WebSocket `market_stats` frame publishes it as `openInterestLimit`, with what is left in `openInterestHeadroom`.
An admitted order fills whatever the mark does afterwards, so a position the mark carries past a cap is not closed: it can only be reduced. A reduce-only order, or any step that reduces a position without flipping it, always passes.
**Where to read it:** `GET /api/v1/market/pairs/{id}/perp/config` (`getPerpMarketConfig`) returns `riskTiers[]` ascending by `lowerBoundNotional`, each with `tierLevel`, `lowerBoundNotional`, `maxLeverage`, `initialMarginRatio`, `maintenanceMarginRatio`, `initialMarginDeduction`, `maintenanceMarginDeduction`, and `maxPositionNotional` (the tier's USD notional cap: the next tier's `lowerBoundNotional`, or the market's position cap on the top tier, absent when uncapped). `maxPositionSize` is deprecated and no longer set. The top-level `maxLeverage`, `initialMarginRatio`, and `maintenanceMarginRatio` fields are tier 1. Position, portfolio, and margin-account endpoints use the same ladder for maintenance margin and liquidation prices. Their position initial-margin requirements also retain the higher selected-leverage requirement described above.
Launch ladder
Tier 1 is 50x on every market at launch; tier 2 varies by sector. The ladder is configured per market and can change — treat this table as a snapshot and read `riskTiers` from `getPerpMarketConfig` for the authoritative, current ladder.
| Sector | Symbols | Tier 1 | Tier 2 |
| - | - | - | - |
| Crypto majors | BTC, ETH, SOL | 50x | 25x above \$100M |
| Equity tier 1 | SPX, AAPL, MSFT, META, AMZN, GOOG, TSLA, TSM, NVDA | 50x | 25x above \$100M |
| Crypto alts | HYPE, SEI, LIT, FARTCOIN | 50x | 5x above \$50M |
| Equity tier 2 | GME, MSTR, COIN, HOOD, PLTR, OKLO, AMD, INTC, MU, MRVL, SNDK, CBRS, SOXL, SPCX | 50x | 10x above \$50M |
| Commodities | GOLD, SILVER, COPPER, NATGAS, PYTHOIL | 50x | 10x above \$50M |
| FX | EUR, GBP, USDBRL, USDINR, USDKRW | 50x, single tier | — |
# Self-Trade Prevention
Source: https://docs.0xmonaco.com/trading-mechanics/self-trade-prevention
Choose what happens when an incoming order reaches your own resting order.
Self-trade prevention (STP) applies across your **wallet family**: accounts sharing your wallet address, including across applications. Those accounts cannot trade with one another. A sub-account registered under a different wallet address belongs to a separate family.
The incoming order’s mode governs the match. By default, Monaco cancels your resting order and continues matching the incoming order against other liquidity.
## Choose A Mode
| Mode | Your resting order | Your incoming order |
| - | - | - |
| Cancel maker · default | Each of your resting orders reached by the incoming order is cancelled in full. | Continues against other liquidity. |
| Cancel taker | Remains untouched. | Any earlier third-party fills stand; the remainder cancels at the first self-touch. |
| Cancel both | Every resting order it would have reached is cancelled. | Its remaining quantity is cancelled. |
| Skip | Remains untouched and is never matched. | Trades around your orders. A remainder that would rest crossing your surviving order is cancelled. |
Default behaviorYour incoming buy reaches your own resting sellThe resting sell is cancelled. The buy continues looking for other liquidity. The two orders do not trade with one another.
## Which Setting Applies?
The priority is:
1. The mode specified on the incoming order.
2. Your configured wallet default.
3. The platform default: **Cancel maker**.
A replacement is a new order, so its mode must be specified again to retain a per-order setting. Wallet defaults apply to every order in the family that does not specify a mode.
## Exceptions To Understand
* **FOK can still remove your quotes.** Under Cancel maker, your own quotes are cancelled before the third-party depth check. If the FOK cannot fill, those cancellations can still stand.
* Under **Cancel taker or Skip**, a self-cross in the fill path cancels the whole FOK. FOK with **Cancel both** and a self-cross is rejected at placement.
* A zero-fill market order refused before sequencing does not commit a simulated maker cancellation. Do not infer a quote was removed solely from the incoming order’s failure.
* **Post-only** orders reject if they would cross before STP is consulted, regardless of who owns the opposing liquidity.
* **Liquidations and auto-deleveraging** do not apply STP. A liquidation slice fills against your own resting orders in other risk buckets and sub-accounts in price order, like anyone else's, and never cancels them; ADL fills no resting orders and can close an opposing position in your other risk buckets. User settings cannot obstruct either.
## Identifying A Prevented Trade
STP cancellations carry a self-trade-prevention reason and the counterparty order ID. They have cancelled status, which differs from accepted zero-fill IOC/FOK rejections for insufficient qualifying liquidity.
The full reference preserves the exact status, event, movement-channel, and configuration fields for integrations.
Self-trade prevention (STP) decides what happens when your incoming order would match one of your own resting orders. Most venues fix a single behavior — typically cancelling the resting order — or offer a limited subset. Monaco exposes **all four behaviors**, selectable per order and configurable as a default per wallet:
| Mode | What happens at a self-cross |
| - | - |
| `CANCEL_MAKER` | Every resting order of yours the incoming order reaches cancels in full; the incoming order keeps matching against other liquidity. **The platform default.** |
| `CANCEL_TAKER` | The incoming order fills third-party liquidity ahead of your order, then its remainder cancels at the first touch of your own order — liquidity behind it is never reached. Your resting order is untouched. |
| `CANCEL_BOTH` | The incoming order's remainder cancels **and** every one of your resting orders it would have reached cancels. |
| `SKIP` | The incoming order trades *around* your resting orders — they are never matched and never cancelled. A remainder that would rest crossing one of your own surviving orders cancels instead of resting. |
`SKIP` in particular is rare among venues: it lets a market maker's incoming flow coexist with its own quotes instead of destroying them.
How it resolves
* **The incoming (taker) order's mode governs the whole match.** Modes carried by resting orders are never consulted — the same convention as every major venue.
* **"Your own" means your wallet family**: every account under your wallet address, across applications, including every margin sub-account you open (they all belong to your user). Two accounts under the same wallet address cannot trade with each other. A master-account sub-account linked under a different wallet address is its own family.
* **Resolution order**: the order's own `selfTradePreventionMode` field if present → your wallet's configured default → the platform default (`CANCEL_MAKER`).
* Orders cancelled by STP are announced like any other cancellation, with `terminalReason: SELF_TRADE_PREVENTION` on the WebSocket event and on later order reads; the event also carries `stpCounterpartyOrderId` identifying the order on the other side, so the cause is always attributable.
* An STP outcome is a **`CANCELLED`** order even when nothing filled — deliberately distinct from an accepted zero-fill `IOC`/`FOK`, which is `REJECTED` for want of qualifying liquidity — for a `FOK` that means depth that cannot cover the full quantity, whose candidate fills are then discarded, not necessarily an empty book. (A plain zero-fill `MARKET` submit never reaches either outcome: it is refused before acceptance and emits no order event.) Status alone separates "my own prevention ended this" from "there was not enough qualifying liquidity", with no message parsing.
* A swept **spot** maker also receives the movements-channel `user_movement` frame for its lock release, exactly as a user-initiated cancel produces. A swept **margin** maker receives none: its reserve release moves no `user_balances` leg — and a user cancel of a margin order emits none either, so the two stay consistent. Do not wait on a movements frame for a margin order.
Setting it
* **Per order**: pass `selfTradePreventionMode` on create, replace, or batch items. A replacement is a new order — restate the field to keep it.
* **Per wallet**: [`GET`](/api-reference/accounts/get-self-trade-prevention-default)/[`PUT /api/v1/accounts/self-trade-prevention`](/api-reference/accounts/set-self-trade-prevention-default) reads and sets your wallet's default (applies to every order from your wallet family that names no per-order mode).
Edge semantics worth knowing
* **Fill-or-kill**: under `CANCEL_TAKER` or `SKIP`, any self-cross in the fill path cancels the whole FOK; under `CANCEL_MAKER`, your makers cancel first and the order then fills or kills against the rest; a `FOK + CANCEL_BOTH` order with a self-cross in its fill path is rejected at placement with `SELF_TRADE_NOT_ALLOWED` — all-or-none contradicts the partial outcomes `CANCEL_BOTH` produces.
* **A killed `FOK` still cancels your makers.** Under `CANCEL_MAKER` the sufficiency check runs on third-party depth only — your own orders are cancelled first and never counted as available liquidity. If that depth cannot fill the order the `FOK` is rejected *and* those cancellations stand: you lose the quotes and get nothing. (`FOK` is a `LIMIT`-only time in force — a `MARKET` order carrying any `timeInForce` is rejected outright.)
* **A rejected taker does not mean your quote was swept.** The cancellation is decided during simulation and commits only if the order 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**, so the simulated cancellation is discarded and your resting order survives. Reconcile against the order stream rather than inferring from the taker's result.
* **Post-only orders** reject with `POST_ONLY_WOULD_CROSS` before STP is ever consulted, whoever owns the crossing liquidity.
* **Liquidations and auto-deleveraging** apply no self-trade prevention at all, whatever your configured mode. A liquidation slice matches your own resting orders — from your other risk buckets and sub-accounts — in price order exactly as it matches anyone else's, counts them as available depth, and never cancels them for crossing. ADL does not trade against the book: it closes opposing positions directly at the pinned price, and a position your other risk buckets hold is as eligible as anyone else's. It never fills your resting orders, but when it fully closes a position, that risk bucket's resting reduce-only orders on the pair are cancelled with terminal reason `AUTO_DELEVERAGING`. User configuration can never impede a forced close. (The liquidated risk bucket's own resting orders are cancelled before it is liquidated, with terminal reason `LIQUIDATION`.)
# Orderbook
Source: https://docs.0xmonaco.com/trading-mechanics/spot
Choose an order type and understand how it can fill.
Monaco uses a central limit order book. The best price executes first. At the same price, older resting orders execute before newer orders.
Cancel requests are prioritized ahead of new orders. Monaco targets sub-millisecond matching and settles on Sei, whose single-slot finality is designed for confirmations in less than 400 ms.
## Market Or Limit?
| Order | What it does | What to expect |
| - | - | - |
| Market | Seeks immediate execution against available liquidity within a protective band. | It can fill partially; any remainder outside the band is cancelled. |
| Limit | Sets a quantity and a price limit. | It can rest in the book or fill immediately against available orders. |
| Conditional | Activates when the mark price meets a condition. | TP/SL orders convert to market orders. |
A limit order is not necessarily a maker order: it may take existing liquidity immediately. See [Fees](/fees) for how maker and taker fills differ.
## Market-Order Price Protection
Standard market orders have a mandatory **1,000 bps (10%)** protective band around a reference price. The reference is the mark price on margin markets, where a perp market can replace the 1,000 bps with its own band (see [Perp price band](/trading-mechanics/perps#perp-price-band)). Otherwise, it is the median of best bid, best ask, and the recent trade price when the book is two-sided with a spread no wider than 1,000 bps; a missing or wider book uses the recent trade price alone.
The recent trade price is the median of the last five liquidity-taking orders, counting each order once at its last fill price. New markets pad missing observations with their oldest one, so it takes three taking orders at a new level for the reference to follow.
| Available liquidity | Result |
| - | - |
| Enough quantity inside the band | The order can fill within the band. |
| Only some quantity inside the band | That portion settles; the remainder is cancelled. |
| No quantity inside the band | The order is rejected for insufficient liquidity. |
| No usable reference price | The order is rejected as unavailable. |
A client can tighten the band but cannot widen it beyond 10%. Limit orders are not subject to this market-order band.
A protective band limits execution prices. It does not guarantee that the entire order will fill.
## How Long An Order Stays Active
| Instruction | Meaning |
| - | - |
| Fill-or-kill (FOK) | Execute the full quantity at the limit price or cancel. |
| Immediate-or-cancel (IOC) | Execute the immediately available portion and cancel the rest. |
| Good-till-cancelled (GTC) | Keep a limit order active until it fills or is cancelled, including across sessions. |
GTC does not apply to market orders. The [Order Management reference](/developers/trade) contains payload requirements, supported combinations, and order management behavior.
## Previewing An Order
Risk simulations use the same protective band and matching walk as placement. Accepted previews report simulated quantity, average and worst fill prices, slippage, and any remainder. The full reference preserves the response fields and implementation details.
Market Design
Monaco uses an orderbook built for fast, transparent execution:
* **Central Limit Order Book (CLOB):** Monaco uses a central limit order book to facilitate trades.
* **Price-time priority:** Better prices are executed first. At the same price level, older resting orders execute before newer orders.
* **Prioritized cancels:** Cancel requests are handled ahead of new order creation, so traders can avoid stale exposure, especially in volatile market moves.
* **Low-latency matching:** Monaco's custom matching engine targets sub-millisecond order-matching.
* **Fast settlement and finality:** Monaco settles on Sei, which provides single-slot finality designed for confirmations in `<400ms`.
For account setup, see [Funding Accounts](/accounts). For deposit flows, see [Deposit tokens](/deposit/deposit-tokens).
Order Types
* **Market orders:** Execute immediately against available liquidity inside a protective price band (see [Market-order price protection](#reference-market-order-price-protection) below). They prioritize execution over price, but the band bounds how far the fill price can move.
* **Limit orders:** Specify both quantity and a price limit. They can rest in the order book, or fill immediately if they cross available orders.
* **Conditional orders:** This includes take-profit and stop-loss (TP/SL) orders, which convert to a market order when the mark price meets a condition.
Use [Order Management](/developers/trade) for order payloads and order management behavior.
Market-order price protection
Every market order executes inside a mandatory server-side protective price band of 1,000 bps (10%) around a robust reference price — the market's mark price on margin markets; otherwise the median of best bid, best ask, and the recent trade price when the book is two-sided and not degenerate (its bid-ask spread is at most 1,000 bps); otherwise the recent trade price alone. The recent trade price is the median of the prices set by the market's last five liquidity-taking orders, not the single last trade, so one or two taking orders that trade far from the market do not move the band; a real move is followed from the third taking order at the new level. Each taking order counts once, at the price of its last fill, so an order that sweeps three price levels contributes one, not three, and the resting orders it fills add nothing. A market with fewer than five taking orders so far counts its oldest one in the empty places, so even a newly listed market needs three taking orders at a new level before the band follows. Fills that would occur beyond the band are cut off:
* The portion that fills inside the band settles; any remainder is cancelled, so the order comes back partially filled.
* If no quantity is available inside the band, the order is rejected as insufficient liquidity.
* A market order on a market with no usable reference price (no mark, a degenerate book, and no trade history) is rejected as unavailable.
A client `slippageToleranceBps` can only *tighten* this band; it can never widen it past the market's band. Limit orders are unaffected — use a limit order to trade beyond the band. The order-risk simulation endpoints run the same walk under the same band and surface the same rejections, so a preflight check matches placement — and because that same simulated match is what admits the order, they also return what it would execute at: `expectedMatchResult`, the engine's simulated fill in the same `MatchResult` shape a placement returns (`totalFilled`, `remainingQuantity` — the band-cut / partial-fill indicator — `averageFillPrice`, `executionPriceRange.worstPrice`, `actualSlippageBps`), plus `referencePrice` for a MARKET preview, priced under your `slippageToleranceBps` merged with the band. Present only on an accepted preview. See [Pre-trade simulation](/sdk/typescript/margin-accounts#pre-trade-simulation).
Time in Force (TIF)
* **Fill-or-kill (FOK):** The entire order quantity must be executed at a given limit price, or it is cancelled. This is most suitable for large block trades and specific hedging mechanisms.
* **Immediate-or-cancel (IOC):** This executes any available portion of the order, regardless of market order or limit order, and cancels the rest. This reduces the risk of resting orders in fast-moving markets.
* **Good-till-cancelled (GTC):** Remains active until executed or manually cancelled, and applies only to limit orders, not market orders. This persists across sessions.
Use [Order Management](/developers/trade) for order payloads and time-in-force behavior.
# TWAP Orders
Source: https://docs.0xmonaco.com/trading-mechanics/twap-orders
Spread execution over time, with control over size, duration, and price.
A TWAP order divides a trade into smaller orders across a time window. Submit one parent order; Monaco schedules and tracks the child orders on the server.
Spreading execution trades immediacy for an average across the window. **The full quantity may not fill.** Execution stops at the window end and reports any shortfall.
## Choose An Execution Style
| Style | How each slice trades | Default cadence |
| - | - | - |
| Taker | Executes a market child within the protective band; the child does not rest. | About every 5 seconds. |
| Passive | Rests a post-only child at the same-side best price, reprices it, then takes the remainder at the slot deadline. | About every 60 seconds. |
Taker is the default. Passive can produce both maker and taker fills. Its maker fills use the pair’s maker fee and carry no application fee; deadline sweeps and crossing takes incur application taker fees.
## Set The Order
| Setting | Rule |
| - | - |
| Market, side, and quantity | Choose the market, buy or sell, and total quantity. The quantity must support at least two valid slices. |
| Execution window | Between 1 minute and 30 days by default. Use a duration, or an end time with an optional start time. |
| Limit price | Optional maximum buy price or minimum sell price for every child. |
| Slippage band | 100 bps (1%) by default; at most 500 bps (5%) around the reference price. |
| Randomization | On by default: slice size and timing vary by up to ±30%. |
| Trading mode | Spot or margin. Margin orders also support reduce-only. |
All timestamps use UTC with a `Z` suffix. At most **25** parents can be open per user, including future-start orders. Delegated-agent sessions cannot create, cancel, fetch, or list TWAP parents in v1.
The schedule is capped at **2,880 slices**. Longer windows stretch the interval: a 30-day taker window uses roughly 15-minute intervals. Market minimums, maximums, and quantity steps also constrain the schedule.
## Price Protection For Passive Orders
The parent limit and slippage band apply to passive quotes as well as taker slices. If a new quote would be outside those bounds, it is not placed. If an older child is already resting, a refused reprice can leave it working at its old price.
Set a limit if you need a hard price bound.
If no mark, usable book reference, or recent trade reference is available, the passive quote’s band check is skipped. The parent limit price is then its only bound.
Inspect both parent progress and open orders. Cancelling the parent removes a resting passive child; cancelling the child directly is not supported.
## If Execution Falls Behind
Funds are checked **per slice**, not locked for the full parent upfront. A slice can be skipped because of insufficient funds or margin, unavailable liquidity, the limit price, or a market halt. A skipped or failed slice does not terminate the parent.
Later slices try to catch up, normally capped at **3×** the base size and never above the market maximum. A minimum-lot remainder exception is described in the full rules. After a gap, the schedule sends one catch-up slice and skips missed slots, rather than sending a burst.
Illustrative outcomeRequested: 10 units · Executed: 7 · Shortfall: 3If the window ends after 7 units fill, the parent is completed with a 3-unit shortfall. The remaining quantity is not traded after the deadline.
## Monitor Or Stop The Order
The parent reports executed quantity, average fill price, progress, placed and skipped slices, and the next planned slice time. **Completed** means either all quantity executed or the window ended. Check executed quantity and shortfall as well as state.
**Cancel the parent to stop execution.** Existing fills stand. A passive child is removed in the same step, with no final taker sweep.
Cancel-all does not stop TWAP parents. Cancelling or replacing an individual child is rejected. Use the parent order as the control.
## Start When A Price Is Reached
A conditional TWAP waits for a reference price to reach or cross a chosen level. It takes a duration; its execution window starts when triggered. An already-met trigger is rejected at creation. An expiry limits the waiting period and defaults to, and is capped at, 30 days from creation.
Margin triggers use the oracle mark, with a venue-reference fallback while waiting; spot triggers use the venue reference. Polling can miss a crossing that reverses between observations. The reference retains exact activation, timestamp, collateral, and expiry semantics.
Once the parent completes or is cancelled, a private execution-quality report compares its average fill price, before fees, with arrival and window benchmarks. Benchmarks without sufficient data are omitted.
A TWAP (time-weighted average price) order executes a large order as a schedule of smaller child orders spread across a time window, so the realized price tracks the market's average over the window rather than the price at a single moment. Monaco runs TWAP execution server-side: you submit one **parent order** with a total quantity and a window, and the matching engine plans, places, and tracks the child orders ("slices") for you. Filling a large order in one print pays for immediacy with market impact; spreading the same quantity across a window trades immediacy for a better average.
TWAP Mechanics at a Glance
* One parent order defines the total quantity, side, market, and execution window; the engine derives a slice schedule from it (roughly one slice every 5 seconds, up to 2,880 slices — windows past 4 hours stretch the interval so the cap holds; a maximum-length 30-day window slices roughly every 15 minutes). The default `TAKER` style targets one slice every 5 seconds; a `PASSIVE` parent targets one every 60 seconds under the same cap.
* Each slice is placed as a market child order with immediate-or-cancel semantics, executing inside a protective price band (100 bps by default). An optional parent limit price additionally caps every child. A `PASSIVE` parent instead rests each slice as a post-only maker child at the touch and takes only the remainder at the slot deadline — see [Passive Execution](#passive-execution).
* Slice sizes and timing are randomized by default (±30% size, ±30% timing) using a recorded seed, so the schedule is reproducible for audit but unpredictable to the market.
* Every slice passes normal risk and balance checks individually. A skipped or failed slice never cancels the parent — the deficit flows into later slices via catch-up.
* Execution stops hard at the window end. Any unfilled remainder is reported as shortfall; it is never traded past the window.
Parameters
A TWAP parent order takes:
* **Market, side, and quantity:** the trading pair, BUY or SELL, and the total quantity to execute. The quantity must be at least twice the pair's minimum order size so it can be sliced, and no larger than the window's slice budget can carry with every child at or below the pair's maximum order size — an oversized parent is rejected at creation instead of accepted into a shortfall it could never avoid.
* **Execution window:** exactly one of a duration or an end time. Duration runs from launch; an end time can be paired with an optional start time (default: now) to schedule the window in advance. The window must be between 1 minute and 30 days by default. Timestamps are **UTC only** — RFC 3339 with a `Z` suffix (e.g. `2026-08-01T10:00:00Z`); a non-UTC offset is rejected rather than converted, and every timestamp the API and WebSocket return is UTC.
* **Limit price (optional):** a parent-level price cap. BUY children never execute above it, SELL children never below it. When the market trades through the limit, slices are skipped rather than executed at a worse price. Omitted, only the slippage band protects child orders.
* **Slippage tolerance (optional):** the per-slice protective price band in basis points around the reference price. The default is 100 bps (1%) and the maximum is 500 bps (5%) — deliberately tighter than the standard [market-order protection band](/trading-mechanics/spot#market-order-price-protection), because a TWAP slice should never sweep deep into the book.
* **Randomize (optional, default on):** applies seeded randomization to slice sizes and timing. Turned off, the schedule is exactly uniform.
* **Execution style (optional, default `TAKER`):** how each slice reaches the book. `TAKER` fires a banded market child at every slot — the behaviour described under [Per-Slice Execution](#per-slice-execution). `PASSIVE` rests a post-only child at the touch and sweeps only what the slot deadline forces, trading immediacy for maker fees; see [Passive Execution](#passive-execution).
* **Trading mode:** SPOT or MARGIN. Margin parents also take the usual margin context (margin account, risk bucket or margin mode, leverage) and support reduce-only, which child orders inherit and clamp to the live position. The risk bucket does not have to exist yet, and nothing moves at creation. Like a first-use market order, a missing (or closed) isolated bucket is created at zero by the parent's first slice, and a missing cross bucket is provisioned empty at creation. Every slice draws what it needs from the margin account's free collateral as it reserves, so the whole parent's margin is never earmarked ahead of need; a slice the account cannot cover (the mark has moved, or the collateral is committed elsewhere) is skipped and reported, never fatal to the parent. A reduce-only parent still requires an existing bucket, since there is no position to reduce without one.
* **Client TWAP ID (optional):** a client-assigned idempotency key, unique per user and application — the server's enforced uniqueness scope. Submitting a duplicate returns HTTP 409 rather than creating a second parent, so a retried request cannot double an execution. Keys are single-use forever within that scope: a key stays reserved even after its parent completes or is cancelled.
The Slice Schedule
At creation, the engine derives a slice plan from the window and quantity: a slice count targeting roughly one slice every 5 seconds — 60 seconds for a `PASSIVE` parent, whose children have to rest long enough to earn queue position — capped at 2,880 slices, and clamped so that every slice — even after size randomization — stays between the pair's minimum and maximum order sizes. Slice sizes are floored to the pair's quantity step, and the final slice is the exact remaining quantity (itself always a placeable size), so the plan always sums exactly to the requested quantity and can never over-allocate or leave an unplaceable tail. A window or quantity that cannot fit at least two slices is rejected at creation, as is a quantity too large for the window's slice budget to carry.
With randomization on, each slice's size is jittered within ±30% and its fire time within ±30% of the base interval. The jitter is derived from a random seed minted at creation and recorded durably: the schedule never depends on wall-clock randomness, so it can be re-derived exactly for verification and audit, while remaining unpredictable to anyone watching the book. Uniform, evenly spaced slices of equal size are easy to detect and trade ahead of; jitter removes that signature.
Per-Slice Execution
This section describes the default `TAKER` execution style; [Passive Execution](#passive-execution) covers `PASSIVE`. Each slice is placed as a market child order with immediate-or-cancel semantics — child orders execute immediately against the book inside their protective band and never rest. There is no upfront lock of the full parent notional. Instead, every slice individually passes the same admission checks as a standalone order at the moment it fires:
* **Spot slices** check the available balance for that slice's quantity.
* **Margin slices** pass normal per-slice margin admission against the risk bucket the parent snapshotted at creation (provisioned then if it did not exist), topping it up from the margin account's unallocated collateral when a slice needs more than the bucket holds.
A slice can be skipped: no liquidity inside the protective band, a parent limit price through the market, a halted market, or failed balance or margin admission. Self-trade prevention is no longer among them as a refusal in its own right. A slice that reaches the committed match walk and meets one of the parent owner's own resting orders resolves under the owner's [self-trade-prevention mode](/developers/trade#self-trade-prevention): under the default `CANCEL_MAKER` the owner's resting order is cancelled — surfacing as an `OrderCancelled` with `terminalReason` `SELF_TRADE_PREVENTION` on the order channels, not as a skip on the parent — and the slice proceeds.
A slice whose only in-band depth **is** the owner's own order is mode-dependent. Under `CANCEL_MAKER` or `SKIP` the child sees no executable third-party depth — liquidity validation excludes your own resting orders — so it is refused before sequencing and the parent records an ordinary insufficient-liquidity skip; nothing of yours is cancelled. Under `CANCEL_TAKER` or `CANCEL_BOTH` the child is instead `CANCELLED` by the dispatch itself and carries no unfilled reason, so validation accepts it, it is sequenced, and the parent counts it as placed — with `CANCEL_BOTH` also cancelling the maker. And under `CANCEL_TAKER` or `CANCEL_BOTH` a child stopped at the first self-touch comes back `CANCELLED`; TWAP counts any non-`REJECTED` child as placed, so that slice increments `slicesPlaced` rather than counting as a skip. It is not necessarily a zero-fill slice either — fills made against third parties before the first self-touch stand and are recorded as that slice's quantity; only a self-touch before any fill leaves it at zero. Monitoring that inferred self-trade from `slice_skipped` should read the order channels instead. Skipped and failed slices are counted on the parent and nothing more — **they never terminate it**. There is no consecutive-failure kill: the schedule keeps attempting until the window ends, and the unexecuted quantity flows into catch-up.
Passive Execution
Set `executionStyle` to `PASSIVE` and each slice is worked as a maker order instead of taken. At the start of a slot the engine rests a post-only `LIMIT` `GTC` child at the same-side touch — the best bid for a BUY, the best ask for a SELL — and leaves it there to earn queue position. As the touch moves, the child chases it with cancel/replace reprices, throttled to at most one attempt per second so a fast book cannot turn into a reprice storm. At the slot's deadline, whatever the child has not earned is swept as a banded `MARKET` child, exactly like a `TAKER` slice. The parent ends up maker wherever the book allowed it and taker only for what the window forced.
The two price bounds are the same ones a `TAKER` slice respects, and the engine does not post outside either: the parent's own `limitPrice`, and the parent's `slippageToleranceBps` band around the **protective reference price** — the oracle mark on a margin market when one is available, otherwise a robust book-derived venue price (from the best bid/ask, when the spread is not degenerate) falling back to the recent trade price, the median of the market's last five trades. Spot parents always use that book-derived chain, since spot has no oracle mark. There is one exception worth knowing if you lean on the band for protection: when the engine can resolve **no** reference at all — a one-sided book on a pair with no trade history, so no mark, no trustworthy two-sided venue price and no recent trade price — the band check is skipped rather than failing closed, and `limitPrice` becomes the only bound on the resting quote. Set one on a passive parent if you need a hard ceiling in a market that thin.
The engine does not clamp to the bound — when the same-side touch lies outside it the engine refuses to quote there, and what that leaves on the book depends on whether a child is already resting. **With no child resting**, nothing is posted: the slot stays open for the next attempt or the deadline sweep, since an off-market child would only sit there unfilled. **With a child already resting**, the reprice is refused *before* the cancel, so that child **stays on the book at its old price** — deliberately, because pulling it with nothing to put back is worse than leaving a stale quote. So a bounded passive parent in a market that has run away does not necessarily have zero resting exposure: it may still have its last in-bound child working, at a price the market has left behind. Read the parent's slice counters and your open orders rather than assuming the book is clear — and stop it by cancelling the parent, since the child itself is not directly cancellable. If instead the passive target price would *cross* the book — the other side is already offering at or better than where the child would rest — resting would be pointless, so the placement executes immediately as a banded taker.
Passive parents plan on a slower cadence: a 60-second target slice interval by default against the taker default of \~5 seconds, because a child that is repriced every few seconds never sits in the queue long enough to be filled. The 2,880-slice cap is unchanged, so a long window stretches its interval rather than adding slices. Both the cadence and the reprice throttle are operator-tunable.
**Cancellation is cancel-only.** A `PASSIVE` parent may have one child resting when you cancel it. That child is removed in the same atomic step that terminalizes the parent: fills it already earned stand, the rest of the quantity is simply released, and nothing is left on the book. No final sweep is fired on a user cancel — cancelling means stop, not "finish what you can".
**Fees follow each fill's own side.** A passive maker fill pays the pair's maker fee and carries **no application fee**, because the application fee is a taker-side fee. Deadline sweeps and crossing takes pay application taker fees like any other taker execution. That is the whole point of the style: on a market where the maker fee is a rebate, a parent that rests most of its quantity can execute at a better all-in cost than the same parent sweeping every slot.
On the `twap_orders` WebSocket channel, a passive parent adds three reasons to the usual lifecycle: `passive_child_placed` (a post-only child now rests at the touch), `child_repriced` (the reprice step ran), and `passive_fill` (maker fills on the resting child, folded into the parent). Deadline sweeps and crossing takes still arrive as `slice_placed`, so a client that only tracks slice counters keeps working unchanged.
Two of those reasons are less literal than they look. `child_repriced` covers the cancel-only fallback as well: when the old child was cancelled and the repost did not happen, the same reason is emitted with nothing resting and the slot left open — so treat it as "the reprice step ran". The frame cannot tell you which happened: the parent payload carries no active-child field, so resting-child status is not observable from this channel at all; follow the child on the ordinary order channels if you need it. And the maker fill that completes the parent arrives as `completed`, not `passive_fill`; there is no final `passive_fill` to wait for.
Everything else is ordinary TWAP behaviour: the same slice plan, randomization, per-slice admission checks, catch-up sizing, hard stop, and shortfall reporting described above.
Falling Behind and Catch-Up
The parent tracks a linear execution target across the window. When execution falls behind — skipped slices, a halted market, or an engine pause — the next slice is sized up to cover the deficit, capped at 3× the base slice size and never above the pair's maximum order size. One exception to the 3× cap: if completing a slice would leave a remainder smaller than the pair's minimum order size — too small for any later slice to ever place — the slice takes the whole remainder instead, exceeding the cap by less than one minimum lot (against a remainder above the pair's maximum it backs off to leave exactly one placeable lot for the next slot rather than exceed the maximum). Randomization and slicing can therefore never strand an unfillable dust remainder; a TWAP that ends short did so because the market would not fill inside the protective band, never because of the schedule's own arithmetic. After a gap, the schedule fires **one** catch-up slice and jumps past all missed slots; it never fires a burst of make-up orders. If the planned schedule is exhausted with quantity remaining, the engine keeps attempting slices at the base cadence until the window closes.
The cap bounds the market impact of any single slice regardless of how far behind the parent is; a long outage is recovered gradually over the remaining window rather than dumped into the book at once.
The Hard Stop
The window end is absolute. When it is reached with quantity outstanding, the parent completes with the shortfall available from the structured fields — the unfilled remainder is exactly `totalQuantity - executedQuantity` — and summarized in the parent's terminal reason (for example, `ended with shortfall 1.5`), which is human-readable text, not a parsing contract. Execution is never extended past the window to chase the remainder — a TWAP order's contract is *this quantity, inside this window, at protected prices*, and the shortfall is the honest report of what that contract could not fill.
Lifecycle
A TWAP parent moves through four states:
* **PENDING:** created; no slices have fired yet (every parent is created PENDING, and a future start time keeps it there until the window opens). A [conditional](#conditional-twap-price-triggered-activation) parent also waits here — indefinitely — until its trigger fires.
* **ACTIVE:** the window is open and the schedule is executing.
* **COMPLETED:** terminal. Either the full quantity executed, or the window ended and the remainder is reported as shortfall in the terminal reason.
* **CANCELLED:** terminal. The owner cancelled the parent; quantity already executed stands.
A conditional parent has one extra way to end: if its trigger never fires by `conditionalExpiry`, the parent goes **CANCELLED** with the terminal reason `trigger expired`, having executed nothing. It leaves nothing behind: a first-use isolated risk bucket is only created by the first slice, so a parent that never sliced never created one.
There is no separate FAILED state — individual slice failures are surfaced through the parent's structured counters (`slicesSkipped` alongside `slicesPlaced`) and the WebSocket `slice_skipped` events, while the lifecycle state stays PENDING/ACTIVE until terminal COMPLETED or CANCELLED. Monitor the counters, not the state, for degraded execution. Cancellation is atomic and immediate: a `TAKER` parent's children never rest on the book, so cancelling the parent stops the schedule with nothing left to unwind, and the parent's fill-so-far is reported as its final execution. A `PASSIVE` parent can have one child resting, and cancellation removes it in that same atomic step — [cancel-only](#passive-execution): earned fills stand, no final sweep fires, and nothing is left behind either.
**The parent is the only control surface.** A slice child is engine-owned machinery rather than an order you placed, so cancelling or replacing one directly is rejected and the message names the parent to act on instead. This is deliberate: a child cancelled on its own leaves the parent running, and the parent sweeps that slot as a taker at its deadline — so an order you believed cancelled comes back and pays taker fees. Cancel the parent and the resting child goes with it. A cancel-all sweep behaves the same way: the slice child is reported as a single refused item naming its parent, and every other order in the sweep still cancels. Cancel-all does **not** stop TWAP parents; only cancelling the parent does.
Conditional TWAP: Price-Triggered Activation
A TWAP parent can be created to sit still and start trading only when the market reaches a price, instead of only at a wall-clock time. Supply `triggerPrice` together with an explicit `triggerDirection` (`ABOVE` or `BELOW`) and the parent is created **PENDING**, watching rather than executing.
* **Reach or cross.** `ABOVE` activates when the reference price is at or above the trigger; `BELOW` when it is at or below. No exact print is required, so a gap straight through the level activates. The condition is evaluated on the engine's own polling cadence rather than on every market tick, so a price that crosses and reverts entirely between two observations is not seen — there is no high/low watermark.
* **The window floats.** A conditional parent takes `durationSeconds` only; `startTime` and `endTime` are rejected, because the window is `[trigger instant, trigger instant + durationSeconds]` and cannot be known at creation. The slice count and interval are derived at creation, and the fire times re-anchor to the trigger instant when it arrives. Until then the create response's `firstSliceAt` and `endTime` are provisional placeholders computed from `[create, create + durationSeconds]`; read the armed window back from the parent (`startTime` / `endTime` / `triggeredAt` on the REST read model, `startAt` / `endAt` on the `twap_orders` WebSocket payload) rather than scheduling on them.
* **Activation is one-way.** Once the parent is trading the trigger is never re-evaluated, and `triggeredAt` records when it fired.
* **Which price.** Margin markets arm against the oracle mark, and a create is rejected while the market has not published one yet — a rejected create moves nothing, so retrying once a mark exists is cheap. Once the parent is waiting, activation falls back to the venue reference price (a book-derived median, then the median of the market's last five trades) when the mark is unavailable, so a mark outage cannot leave a live trigger unwatched. Spot markets use the venue reference price throughout. On a one-sided spot book that means a move through the trigger is seen from its third trade at the new level, not its first.
* **A trigger already met is refused.** Creating a parent whose condition is already satisfied returns a 400 rather than starting immediately — omit `triggerPrice` if immediate execution is what you want.
* **`conditionalExpiry` bounds the wait.** An optional RFC 3339 UTC deadline (`Z` suffix only). If the trigger has not fired by then the parent is CANCELLED with the terminal reason `trigger expired`. It defaults to, and is capped at, the maximum window (30 days) from creation, and fires on the clock alone — no price observation is needed, so a halted or unpriceable market cannot leave a parent waiting past its deadline.
Everything after activation is ordinary TWAP behaviour: the same slicing, randomization, protective band, catch-up, hard stop and cancellation semantics described above.
Limits
* The execution window must be between 1 minute and 30 days by default.
* Each user can have at most 25 open TWAP parents (scheduled or executing) at a time — a future-start parent counts against the cap from creation.
* Both spot and margin markets are supported.
* Delegated-agent sessions cannot use TWAP orders in v1: creating, cancelling, fetching, and listing parents are all rejected.
* TWAP parent orders are private to their owner; they are never visible to other participants.
Monitoring
The parent order is the single view of progress: it reports executed quantity, executed notional, the volume-weighted average fill price, progress as a fraction of the total, slices placed and skipped, the next planned slice time, and the terminal reason once finished. TWAP parents can be fetched individually or listed with state and market filters.
Parents are not rows of the ordinary order listing (`GET /api/v1/orders`), which carries book orders only. Read them from the dedicated TWAP list (`GET /api/v1/orders/twap`), which filters by state and market, or fetch one by ID.
For live updates, the [WebSocket API](/websocket) carries a dedicated authenticated `twap_orders` channel (optionally filtered to one market with `twap_orders:{tradingPairId}`). It delivers the full parent snapshot on every mutation — creation, each placed or skipped slice, cancellation, completion, for a `PASSIVE` parent `passive_child_placed`, `child_repriced` and `passive_fill`, and for a conditional parent `triggered` (the trigger fired and the window armed) and `trigger_expired` (the deadline passed with the trigger unfired) — tagged with the reason for the update, so a client can render live progress without polling. The formatted detail for an expiry (`trigger expired`) rides `terminalReason`, not the reason field. Like the parents themselves, the channel is private: only the owner receives events.
Each child order is a regular order — fills flow through the standard authenticated order channels on the [WebSocket API](/websocket), and child orders record the parent's ID in their `parentOrderId` field. The same field rides every order frame of a child and its `orders` snapshot row, so a parent's children can be tied together in order history or from the socket alone.
Execution Quality (TCA)
Once a parent is terminal — COMPLETED or CANCELLED — a post-execution transaction cost analysis report scores how well the schedule executed. The report compares the realized average fill price (gross of fees, so it compares cleanly against price benchmarks) with two references over the parent's **active window**, from its start to its actual terminal time (never the planned end — a cancelled parent stopped trading at cancellation): the **arrival price**, the reference price as the window opened, and the **window-TWAP benchmark**, the average reference price across the window's sealed minute buckets. Both references come from a single price series named in the report — the 1-minute risk mark for marked pairs, falling back to 1-minute OHLCV closes — and both scores are signed basis points where positive always means the execution beat the reference: a BUY that filled below it, a SELL that filled above it.
The analytics are honest or absent: when nothing executed, no reference series adequately covered the window, or the window was too short to seal a single minute bucket, the affected fields are omitted from the report rather than filled with a defensible-looking number. The structural fields — quantities executed and short, the window bounds, market and side — are always present. Fetch the report with [`sdk.trading.getTwapOrderTca`](/sdk/typescript/trades#twap-orders) (`GET /api/v1/orders/twap/{twapOrderId}/tca`; gRPC `OrdersService.GetTwapOrderTca`); like the rest of the TWAP surface it is owner-private and rejects delegated-agent sessions.
For order payloads and order management behavior, use [Order Management](/developers/trade).