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

# Trading Hooks

> Placing and managing orders, the live order list, and perps positions.

## useTrade

```tsx theme={null}
import { usePositions, useTrade } from "@0xmonaco/react";

const {
  placeLimitOrder,
  placeMarketOrder,
  cancelOrder,
  batchCancel,
  batchCancelAll,
  cancelAllAfter,
  replaceOrder,
  batchCreate,
  batchReplace,
  cancelConditionalOrder,
  listConditionalOrders,
  getPaginatedOrders,
  getOrder,
} = useTrade();
```

The hook returns the same surface as [`sdk.trading`](https://docs.0xmonaco.com/sdk/typescript/trades).

### Place Orders

**placeLimitOrder** `(pairId, side, quantity, price, options?) =\> Promise\<CreateOrderResponse\>`

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"`
* `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](https://docs.0xmonaco.com/developers/trade.md#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](https://docs.0xmonaco.com/developers/trade.md#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](https://docs.0xmonaco.com/developers/trade.md#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 15 minutes. See [Safe Retries](https://docs.0xmonaco.com/developers/trade.md#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`](https://docs.0xmonaco.com/sdk/react/hooks/trading#usepositions). 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](https://docs.0xmonaco.com/developers/trade.md#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" },
});
```

**placeMarketOrder** `(pairId, side, quantity, options?) =\> Promise\<CreateOrderResponse\>`

Place a market order. Market orders execute inside a mandatory server-side price band, 1,000 bps by default (see [Market-order price protection](https://docs.0xmonaco.com/trading-mechanics/spot.md#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](https://docs.0xmonaco.com/developers/trade.md#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](https://docs.0xmonaco.com/developers/trade.md#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 15 minutes. See [Safe Retries](https://docs.0xmonaco.com/developers/trade.md#safe-retries-with-idempotency-keys)

### Order Management

**cancelOrder** `(orderId) =\> Promise\<CancelOrderResponse\>`

Cancel an existing order

**batchCancel** `(orderIds: string[]) =\> Promise\<BatchCancelOrdersResponse\>`

Cancel specific orders by their IDs.

**Returns:**

* `totalRequested`: number
* `totalCancelled`: number
* `totalFailed`: number
* `results`: `BatchCancelResult[]` — each with `orderId`, optional `cancelledAt`, optional `error: { code, message }`

`results` has one entry per requested ID, in request order, so `totalRequested` is always the number of IDs sent. An ID that is not one of your live orders fails with `ORDER_NOT_FOUND`. 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}`);
  });
}
```

**batchCancelAll** `(tradingPairId?: string) =\> Promise\<BatchCancelOrdersResponse\>`

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`);
}
```

**cancelAllAfter** `(timeoutMs: number, options?: \{ tradingPairId?: string \}) =\> Promise\<CancelAllAfterResponse\>`

Arm, refresh or disarm the cancel-all-after dead man's switch: unless you call again before `timeoutMs` runs out, your resting orders are cancelled with `terminalReason` `CANCEL_ALL_AFTER`. `0` disarms; otherwise `5000` to `900000` ms. See [`cancelAllAfter`](https://docs.0xmonaco.com/sdk/typescript/trades) for what is cancelled and the errors.

**Example:**

```tsx theme={null}
const { cancelAllAfter } = useTrade();
// One queue for every call this component makes, kept across effect runs:
// each call starts after the previous one settles. A refresh then cannot
// land after the disarm, and a cleanup's disarm cannot land after the next
// setup's arm (Strict Mode runs setup, cleanup, setup on mount).
const queue = useRef<Promise<unknown>>(Promise.resolve());

useEffect(() => {
  const send = (timeoutMs: number) => {
    queue.current = queue.current.then(() => cancelAllAfter(timeoutMs)).catch(() => undefined);
  };
  send(30_000);
  const refresh = setInterval(() => send(30_000), 10_000);
  return () => {
    clearInterval(refresh);
    send(0);
  };
}, [cancelAllAfter]);
```

**replaceOrder** `(orderId, newOrder) =\> Promise\<ReplaceOrderResponse\>`

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`](https://docs.0xmonaco.com/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](https://docs.0xmonaco.com/developers/trade.md#client-order-ids).

### Batch Operations

**batchCreate** `(orders: BatchCreateOrderParams[]) =\> Promise\<BatchCreateOrdersResponse\>`

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](https://docs.0xmonaco.com/developers/trade.md#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](https://docs.0xmonaco.com/developers/trade.md#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](https://docs.0xmonaco.com/developers/trade.md#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](https://docs.0xmonaco.com/developers/trade.md#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`);
      }
    });
  }
}
```

**batchReplace** `(orders: BatchReplaceOrderParams[]) =\> Promise\<BatchReplaceOrdersResponse\>`

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](https://docs.0xmonaco.com/developers/trade.md#client-order-ids)
* `postOnly?`: boolean — maker-only intent for the replacement; never inherited from the original. See [Post-Only Orders](https://docs.0xmonaco.com/developers/trade.md#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](https://docs.0xmonaco.com/developers/trade.md#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`](https://docs.0xmonaco.com/sdk/react/hooks/trading#usepositions). The hook below covers listing and cancelling conditional orders. Concepts and lifecycle: [Order Management](https://docs.0xmonaco.com/developers/trade.md#conditional-perps-orders).

**cancelConditionalOrder** `(conditionalOrderId: string) =\> Promise\<CancelConditionalOrderResponse\>`

Cancel an active conditional order.

**listConditionalOrders** `(params?: ListConditionalOrdersParams) =\> Promise\<ListConditionalOrdersResponse\>`

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

**getPaginatedOrders** `(params?) =\> Promise\<GetPaginatedOrdersResponse\>`

Fetch paginated orders

**getOrder** `(orderId) =\> Promise\<GetOrderResponse\>`

Get order details by ID

## useUserOrders

> [!NOTE]
>
> **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

**maxOrders** `number` (default: 50)

Maximum number of orders to keep in state (configurable history limit)

### Return Values

**orders** `Order[]`

Array of user orders, automatically updated in real-time. Merged from REST API initial data and WebSocket updates.

A TWAP's slices — the child orders a TWAP parent places, which carry `parentOrderId` — are not in this list, from any source: the order listing leaves them to their parent, so the hook drops slice rows from the REST page, from the `orders` snapshot and from live frames alike. Read a TWAP's slices with [`sdk.trading.listTwapOrderSlices`](https://docs.0xmonaco.com/sdk/typescript/trades#twap-orders).

**loading** `boolean`

`true` while fetching initial orders from REST API, `false` once loaded

**subscribed** `boolean`

`true` when WebSocket subscription is active, `false` otherwise

**error** `Error | null`

Error object if something went wrong during fetch or subscription, `null` otherwise

**clearError** `() =\> void`

Function to clear the current error state

**refresh** `() =\> Promise\<void\>`

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](https://docs.0xmonaco.com/sdk/typescript/websockets.md#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.

`origin` says what placed an order: `"USER"`, `"LIQUIDATION"`, `"ADL"` or `"SYSTEM"`. A liquidation is a reduce-only IOC limit order, so `orderType`, `timeInForce` and `reduceOnly` read the same as an order the user placed; render the row as a liquidation from `origin === "LIQUIDATION"`. The hook takes it from the `OrderPlaced` acknowledgement, from every later order frame (fills, cancels, expiry), from every subscribe-time snapshot row and from every REST row, so an order the user placed reads `"USER"` from whichever arrives first. A frame only fills in a missing origin; it never replaces one the row already has. Because it never changes for an order, whichever side knows it supplies it: a row from a server that does not report it gains it on the next REST read, and a row that learned it from the socket keeps it against a REST row that does not report one. If both report one and they differ, the REST row is the stored order and its value stands. Absent means not reported, never `"USER"`.

`placementTakerFeeBps` and `placementMakerFeeBps` are the Monaco fee rates fixed when the order was placed, in basis points. The hook takes them from the `OrderPlaced` acknowledgement, from subscribe-time snapshot rows and from REST rows. An order placed before rates were stored has neither, and absent never means `"0"`.

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.

> [!NOTE]
>
> 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`](https://docs.0xmonaco.com/sdk/react/hooks/session#useauth) 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 <div>Loading orders...</div>;
  }

  if (error) {
    return <div>Error: {error.message}</div>;
  }

  return (
    <div>
      <p>WebSocket: {subscribed ? "Connected" : "Disconnected"}</p>
      <h2>Your Orders ({orders.length})</h2>
      {orders.map(order => (
        <div key={order.id}>
          <strong>{order.side}</strong> {order.quantity} @ {order.price}
          <span>Status: {order.status}</span>
        </div>
      ))}
    </div>
  );
}
```

### 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 (
    <div>
      <h2>Active Orders ({activeOrders.length})</h2>
      {activeOrders.map(order => (
        <div key={order.id}>
          {order.side} {order.quantity} @ {order.price}
          {order.filledQuantity && order.filledQuantity !== "0" && (
            <span>Filled: {order.filledQuantity}</span>
          )}
        </div>
      ))}
    </div>
  );
}
```

### 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<string, typeof orders>);

  return (
    <div>
      {Object.entries(byPair).map(([pair, pairOrders]) => (
        <div key={pair}>
          <h3>{pair}</h3>
          <p>{pairOrders.length} orders</p>
          <ul>
            {pairOrders.map(order => (
              <li key={order.id}>
                {order.side} {order.quantity} @ {order.price} - {order.status}
              </li>
            ))}
          </ul>
        </div>
      ))}
    </div>
  );
}
```

### 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 (
    <div>
      <div>
        <button onClick={() => setFilter("all")}>All ({orders.length})</button>
        <button onClick={() => setFilter("active")}>Active</button>
        <button onClick={() => setFilter("filled")}>Filled</button>
        <button onClick={() => setFilter("cancelled")}>Cancelled</button>
      </div>
      
      {loading ? (
        <div>Loading...</div>
      ) : (
        <div>
          {filteredOrders.map(order => (
            <div key={order.id}>
              <strong>{order.tradingPairId}</strong>
              <span>{order.side} {order.quantity}</span>
              {order.price && <span>@ {order.price}</span>}
              <span>{order.status}</span>
            </div>
          ))}
        </div>
      )}
    </div>
  );
}
```

### 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 <div>Order not found</div>;
  }

  const fillPercentage = order.filledQuantity && order.quantity
    ? (parseFloat(order.filledQuantity) / parseFloat(order.quantity)) * 100
    : 0;

  return (
    <div>
      <div>
        <span>Live: {subscribed ? "Connected" : "Disconnected"}</span>
      </div>
      
      <h3>Order {order.id.slice(0, 8)}...</h3>
      <p>Status: {order.status}</p>
      <p>Side: {order.side}</p>
      <p>Quantity: {order.quantity}</p>
      {order.price && <p>Price: {order.price}</p>}
      
      {order.filledQuantity && order.filledQuantity !== "0" && (
        <div>
          <p>Filled: {order.filledQuantity} ({fillPercentage.toFixed(2)}%)</p>
          <div style={{ width: "100%", backgroundColor: "#e0e0e0", borderRadius: "4px" }}>
            <div 
              style={{ 
                width: `${fillPercentage}%`, 
                backgroundColor: "#4caf50", 
                height: "8px",
                borderRadius: "4px",
                transition: "width 0.3s ease"
              }} 
            />
          </div>
        </div>
      )}
      
      {order.averageFillPrice && (
        <p>Average Fill Price: {order.averageFillPrice}</p>
      )}
    </div>
  );
}
```

### Error Handling with Retry

```tsx theme={null}
function OrdersWithRetry() {
  const { orders, loading, error, clearError, refresh } = useUserOrders(50);

  if (error) {
    return (
      <div>
        <p>Failed to load orders</p>
        <p>{error.message}</p>
        <button onClick={() => {
          clearError();
          refresh();
        }}>
          Retry
        </button>
      </div>
    );
  }

  if (loading) {
    return <div>Loading orders...</div>;
  }

  return (
    <div>
      <button onClick={refresh}>Refresh Orders</button>
      <div>
        {orders.map(order => (
          <div key={order.id}>
            {order.tradingPairId}: {order.side} {order.quantity}
          </div>
        ))}
      </div>
    </div>
  );
}
```

### 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 (
      <div className="animate-pulse">
        <div className="h-4 bg-gray-200 rounded w-3/4 mb-2"></div>
        <div className="h-4 bg-gray-200 rounded w-1/2"></div>
      </div>
    );
  }

  if (error) {
    return (
      <div className="bg-red-50 border border-red-200 rounded p-4">
        <p className="text-red-800">Failed to load orders</p>
        <p className="text-sm text-red-600">{error.message}</p>
      </div>
    );
  }

  return (
    <div className="space-y-4">
      {/* Connection Status */}
      <div className="flex items-center gap-2">
        <div className={`w-2 h-2 rounded-full ${subscribed ? "bg-green-500" : "bg-gray-400"}`} />
        <span className="text-sm text-gray-600">
          {subscribed ? "Real-time updates active" : "Disconnected"}
        </span>
      </div>

      {/* Stats */}
      <div className="grid grid-cols-4 gap-4">
        <div className="bg-white border border-gray-200 rounded-lg p-4">
          <p className="text-sm text-gray-600">Total Orders</p>
          <p className="text-2xl font-bold">{stats.total}</p>
        </div>
        <div className="bg-white border border-gray-200 rounded-lg p-4">
          <p className="text-sm text-gray-600">Active</p>
          <p className="text-2xl font-bold text-blue-600">{stats.active}</p>
        </div>
        <div className="bg-white border border-gray-200 rounded-lg p-4">
          <p className="text-sm text-gray-600">Filled</p>
          <p className="text-2xl font-bold text-green-600">{stats.filled}</p>
        </div>
        <div className="bg-white border border-gray-200 rounded-lg p-4">
          <p className="text-sm text-gray-600">Cancelled</p>
          <p className="text-2xl font-bold text-gray-600">{stats.cancelled}</p>
        </div>
      </div>

      {/* Orders List */}
      <div className="space-y-2">
        {orders.length === 0 ? (
          <p className="text-gray-500 text-center py-8">No orders yet</p>
        ) : (
          orders.map(order => (
            <div 
              key={order.id}
              className="bg-white border border-gray-200 rounded-lg p-4 hover:shadow-md transition-shadow"
            >
              <div className="flex items-start justify-between">
                <div className="flex-1">
                  <div className="flex items-center gap-2">
                    <span className="font-medium">{order.tradingPairId}</span>
                    <span className={`px-2 py-1 text-xs rounded ${
                      order.side === "BUY" ? "bg-green-100 text-green-800" : "bg-red-100 text-red-800"
                    }`}>
                      {order.side}
                    </span>
                    <span className={`text-sm ${getStatusColor(order.status)}`}>
                      {order.status}
                    </span>
                  </div>
                  
                  <div className="mt-2 text-sm text-gray-600">
                    <span>Quantity: {order.quantity}</span>
                    {order.price && <span className="ml-4">Price: {order.price}</span>}
                    {order.filledQuantity && order.filledQuantity !== "0" && (
                      <span className="ml-4">Filled: {order.filledQuantity}</span>
                    )}
                  </div>
                  
                  {order.averageFillPrice && (
                    <div className="mt-1 text-sm text-gray-500">
                      Avg Fill Price: {order.averageFillPrice}
                    </div>
                  )}
                </div>
                
                <div className="text-right text-sm text-gray-500">
                  {order.createdAt && (
                    <p>{formatDistanceToNow(new Date(order.createdAt), { addSuffix: true })}</p>
                  )}
                </div>
              </div>
            </div>
          ))
        )}
      </div>
    </div>
  );
}

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
  origin?: "USER" | "LIQUIDATION" | "SYSTEM" | "ADL" | (string & {});  // what placed the order; label a liquidation from this, not from orderType
  placementTakerFeeBps?: string;       // Monaco taker fee rate fixed at placement, in bps; absent for orders placed before rates were stored
  placementMakerFeeBps?: string;       // Monaco maker fee rate fixed at placement, in bps (negative = rebate); absent likewise

  // 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](https://docs.0xmonaco.com/sdk/react/hooks/trading.md#usetrade) - Place, cancel, and manage orders
* [useOrderbook](https://docs.0xmonaco.com/sdk/react/hooks/market-data.md#useorderbook) - Real-time orderbook updates
* [WebSocket API](https://docs.0xmonaco.com/reference/websockets.md#orders) - Low-level userOrders WebSocket channel
* [TypeScript SDK](https://docs.0xmonaco.com/sdk/typescript/websockets.md#orders-channel) - Core SDK WebSocket methods

## usePositions

```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`](https://docs.0xmonaco.com/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](https://docs.0xmonaco.com/developers/positions.md).

***

### Read

**listPositions** `(params?: ListPositionsParams) =\> Promise\<ListPositionsResponse\>`

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<Position[]>([]);

  useEffect(() => {
    let cancelled = false;
    (async () => {
      const result = await listPositions({
        marginAccountId: marginAccountId,
        status: "OPEN",
      });
      if (!cancelled) setPositions(result.positions);
    })();
    return () => { cancelled = true; };
  }, [marginAccountId, listPositions]);

  return (
    <ul>
      {positions.map((p) => (
        <li key={p.positionId}>
          {p.side} {p.size} @ {p.entryPrice} — PnL {p.unrealizedPnl}
        </li>
      ))}
    </ul>
  );
}
```

**getPosition** `(positionId: string) =\> Promise\<Position\>`

Full snapshot of a single position. Includes side, size, entry/mark/liquidation prices, isolated margin, leverage, and PnL. See [`Position` type](https://docs.0xmonaco.com/sdk/typescript/positions).

**getPositionRisk** `(positionId: string) =\> Promise\<PositionRisk\>`

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<number | null>(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 <div>Liquidation distance: {(distance * 100).toFixed(2)}%</div>;
}
```

**listPositionHistory** `(params?: ListPositionHistoryParams) =\> Promise\<ListPositionHistoryResponse\>`

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

> [!NOTE]
>
> 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.

**attachPositionTpSl** `(positionId, request) =\> Promise\<AttachPositionTpSlResponse\>`

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](https://docs.0xmonaco.com/developers/trade.md#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](https://docs.0xmonaco.com/developers/trade.md#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

**closePosition** `(positionId, request) =\> Promise\<ClosePositionResponse\>`

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",
});
```

**batchCloseAllPositions** `(request?: BatchCloseAllRequest) =\> Promise\<BatchCloseAllResponse\>`

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](https://docs.0xmonaco.com/sdk/typescript/positions).

**Parameters:**

* `request?`:

  * `tradingPairId?`: string — limit the close to one trading pair. Cannot be combined with `tradingPairIds`
  * `tradingPairIds?`: string\[] — limit the close to any of these trading pairs (1 to 256), in one request. An empty array is rejected; omit the field to close every pair
  * `side?`: `"LONG" | "SHORT"` — limit the close to one side
  * `slippageToleranceBps?`: number — applied to each MARKET close

  With no pair filter, every open position is closed.

**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](https://docs.0xmonaco.com/sdk/typescript/websockets.md).

For order-fill confirmations after a `closePosition` or attached-TP/SL trigger, subscribe to `sdk.ws.userOrders`.


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