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

# Balance & Vault Hooks

> Live balances, balance movements, and vault deposits and withdrawals.

## useUserBalances

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

**balances** `AccountBalance[]`

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

**loading** `boolean`

`true` while fetching initial balances 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\>`

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](https://docs.0xmonaco.com/sdk/typescript/websockets.md#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 spot fills and their fees or rebates
* **Margin Transfers** - The spot leg of collateral moved into or out of a margin account

> [!NOTE]
>
> Every event carries one spot balance row. A versioned 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. An unversioned event, from a producer that does
> not stamp versions yet, is applied fail-open without changing the held spot
> version. Margin collateral never arrives on this channel: a margin-routed
> deposit, a perpetual fill, a funding settlement, a liquidation and an
> auto-deleveraging close send no balance event, so read 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`](https://docs.0xmonaco.com/sdk/react/hooks/session#useauth) 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 <div>Loading balances...</div>;
  }

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

  return (
    <div>
      <p>WebSocket: {subscribed ? "Connected" : "Disconnected"}</p>
      <h2>Your Balances</h2>
      {balances.map(balance => (
        <div key={balance.assetId}>
          <strong>{balance.symbol || balance.token.slice(0, 8)}</strong>
          <div>Available: {balance.availableBalance}</div>
          <div>Locked: {balance.lockedBalance}</div>
          <div>Total: {balance.totalBalance}</div>
        </div>
      ))}
    </div>
  );
}
```

### With Refresh Button

```tsx theme={null}
function BalancesWithRefresh() {
  const { balances, loading, refresh } = useUserBalances();

  return (
    <div>
      <div style={{ display: "flex", justifyContent: "space-between", alignItems: "center" }}>
        <h2>Your Balances</h2>
        <button onClick={refresh} disabled={loading}>
          {loading ? "Refreshing..." : "Refresh"}
        </button>
      </div>

      {balances.map(balance => (
        <div key={balance.assetId}>
          <h3>{balance.symbol || "Unknown Token"}</h3>
          <p>Available: {balance.availableBalance}</p>
          <p>Locked: {balance.lockedBalance}</p>
          <p>Total: {balance.totalBalance}</p>
        </div>
      ))}
    </div>
  );
}
```

### 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 (
    <div>
      <h2>Active Balances ({activeBalances.length})</h2>
      {activeBalances.map(balance => (
        <div key={balance.assetId}>
          <div>
            <strong>{balance.symbol}</strong>
            <span>{balance.totalBalance}</span>
          </div>
          {parseFloat(balance.lockedBalance) > 0 && (
            <div style={{ fontSize: "0.9em", color: "#666" }}>
              Locked: {balance.lockedBalance}
            </div>
          )}
        </div>
      ))}
    </div>
  );
}
```

### 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 <div>Loading balances...</div>;
  }

  return (
    <div>
      <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr 1fr", gap: "1rem" }}>
        <div style={{ padding: "1rem", border: "1px solid #ddd", borderRadius: "8px" }}>
          <p style={{ fontSize: "0.9em", color: "#666" }}>Total Tokens</p>
          <p style={{ fontSize: "2em", fontWeight: "bold" }}>{totalTokens}</p>
        </div>
        <div style={{ padding: "1rem", border: "1px solid #ddd", borderRadius: "8px" }}>
          <p style={{ fontSize: "0.9em", color: "#666" }}>Active Tokens</p>
          <p style={{ fontSize: "2em", fontWeight: "bold" }}>{activeTokens}</p>
        </div>
        <div style={{ padding: "1rem", border: "1px solid #ddd", borderRadius: "8px" }}>
          <p style={{ fontSize: "0.9em", color: "#666" }}>Tokens with Locked Balance</p>
          <p style={{ fontSize: "2em", fontWeight: "bold" }}>
            {balances.filter(b => parseFloat(b.lockedBalance) > 0).length}
          </p>
        </div>
      </div>

      <div style={{ marginTop: "2rem" }}>
        <h3>Balance Details</h3>
        {balances.map(balance => (
          <div
            key={balance.assetId}
            style={{
              padding: "1rem",
              border: "1px solid #eee",
              borderRadius: "4px",
              marginBottom: "0.5rem"
            }}
          >
            <div style={{ display: "flex", justifyContent: "space-between" }}>
              <strong>{balance.symbol || "Unknown"}</strong>
              <span>{balance.totalBalance}</span>
            </div>
            <div style={{ fontSize: "0.85em", color: "#666", marginTop: "0.5rem" }}>
              <span>Available: {balance.availableBalance}</span>
              {parseFloat(balance.lockedBalance) > 0 && (
                <span style={{ marginLeft: "1rem" }}>Locked: {balance.lockedBalance}</span>
              )}
            </div>
          </div>
        ))}
      </div>
    </div>
  );
}
```

### 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 <div>Balance for {tokenSymbol} not found</div>;
  }

  const lockedPercentage = parseFloat(balance.totalBalance) > 0
    ? (parseFloat(balance.lockedBalance) / parseFloat(balance.totalBalance)) * 100
    : 0;

  return (
    <div>
      <div style={{ display: "flex", alignItems: "center", gap: "0.5rem" }}>
        <div style={{
          width: "8px",
          height: "8px",
          borderRadius: "50%",
          backgroundColor: subscribed ? "#4caf50" : "#999"
        }} />
        <span style={{ fontSize: "0.9em", color: "#666" }}>
          {subscribed ? "Live Updates Active" : "Disconnected"}
        </span>
      </div>

      <h3>{balance.symbol || "Unknown Token"}</h3>

      <div style={{ marginTop: "1rem" }}>
        <div style={{ marginBottom: "0.5rem" }}>
          <strong>Total Balance:</strong> {balance.totalBalance}
        </div>
        <div style={{ marginBottom: "0.5rem" }}>
          <strong>Available:</strong> {balance.availableBalance}
        </div>
        <div style={{ marginBottom: "0.5rem" }}>
          <strong>Locked:</strong> {balance.lockedBalance}
        </div>
      </div>

      {parseFloat(balance.lockedBalance) > 0 && (
        <div style={{ marginTop: "1rem" }}>
          <p style={{ fontSize: "0.9em", marginBottom: "0.5rem" }}>
            Locked: {lockedPercentage.toFixed(2)}%
          </p>
          <div style={{ width: "100%", backgroundColor: "#e0e0e0", borderRadius: "4px", height: "8px" }}>
            <div
              style={{
                width: `${lockedPercentage}%`,
                backgroundColor: "#ff9800",
                height: "8px",
                borderRadius: "4px",
                transition: "width 0.3s ease"
              }}
            />
          </div>
        </div>
      )}

      <div style={{ marginTop: "1rem", fontSize: "0.85em", color: "#666" }}>
        <p>Decimals: {balance.decimals}</p>
        <p>Token: {balance.token.slice(0, 10)}...{balance.token.slice(-8)}</p>
      </div>
    </div>
  );
}
```

### Error Handling with Retry

```tsx theme={null}
function BalancesWithRetry() {
  const { balances, loading, error, clearError, refresh } = useUserBalances();

  if (error) {
    return (
      <div style={{ padding: "1rem", backgroundColor: "#fee", border: "1px solid #fcc", borderRadius: "8px" }}>
        <p style={{ color: "#c00", fontWeight: "bold" }}>Failed to load balances</p>
        <p style={{ color: "#666" }}>{error.message}</p>
        <button
          onClick={() => {
            clearError();
            refresh();
          }}
          style={{ marginTop: "1rem" }}
        >
          Retry
        </button>
      </div>
    );
  }

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

  return (
    <div>
      <div style={{ display: "flex", justifyContent: "space-between", marginBottom: "1rem" }}>
        <h2>Balances</h2>
        <button onClick={refresh}>Refresh</button>
      </div>

      {balances.map(balance => (
        <div key={balance.assetId} style={{ marginBottom: "0.5rem" }}>
          <strong>{balance.symbol}:</strong> {balance.totalBalance}
        </div>
      ))}
    </div>
  );
}
```

### 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 (
      <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 balances</p>
        <p className="text-sm text-red-600">{error.message}</p>
        <button
          onClick={refresh}
          className="mt-2 px-4 py-2 bg-red-600 text-white rounded hover:bg-red-700"
        >
          Retry
        </button>
      </div>
    );
  }

  return (
    <div className="space-y-4">
      {/* Connection Status */}
      <div className="flex items-center justify-between">
        <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>
        <button
          onClick={refresh}
          className="px-3 py-1 text-sm border border-gray-300 rounded hover:bg-gray-50"
        >
          Refresh
        </button>
      </div>

      {/* Stats */}
      <div className="grid grid-cols-3 gap-4">
        <div className="bg-white border border-gray-200 rounded-lg p-4">
          <p className="text-sm text-gray-600">Total Tokens</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 Balances</p>
          <p className="text-2xl font-bold text-green-600">{stats.active}</p>
        </div>
        <div className="bg-white border border-gray-200 rounded-lg p-4">
          <p className="text-sm text-gray-600">With Locked</p>
          <p className="text-2xl font-bold text-orange-600">{stats.withLocked}</p>
        </div>
      </div>

      {/* Balances List */}
      <div className="space-y-2">
        <h3 className="text-lg font-semibold">Token Balances</h3>

        {balances.length === 0 ? (
          <p className="text-gray-500 text-center py-8">No balances yet</p>
        ) : (
          balances.map(balance => {
            const hasBalance = parseFloat(balance.totalBalance) > 0;
            const hasLocked = parseFloat(balance.lockedBalance) > 0;

            return (
              <div
                key={balance.assetId}
                className={`bg-white border rounded-lg p-4 ${
                  hasBalance ? "border-gray-200 hover:shadow-md" : "border-gray-100 opacity-60"
                } transition-shadow`}
              >
                <div className="flex items-start justify-between">
                  <div className="flex-1">
                    <div className="flex items-center gap-2">
                      <span className="font-medium text-lg">
                        {balance.symbol || "Unknown"}
                      </span>
                      {hasLocked && (
                        <span className="px-2 py-1 text-xs bg-orange-100 text-orange-800 rounded">
                          Locked
                        </span>
                      )}
                    </div>

                    <div className="mt-2 space-y-1">
                      <div className="flex items-center gap-4 text-sm">
                        <span className="text-gray-600">Total:</span>
                        <span className="font-medium">{balance.totalBalance}</span>
                      </div>
                      <div className="flex items-center gap-4 text-sm">
                        <span className="text-gray-600">Available:</span>
                        <span className="text-green-600">{balance.availableBalance}</span>
                      </div>
                      {hasLocked && (
                        <div className="flex items-center gap-4 text-sm">
                          <span className="text-gray-600">Locked:</span>
                          <span className="text-orange-600">{balance.lockedBalance}</span>
                        </div>
                      )}
                    </div>
                  </div>

                  <div className="text-right text-xs text-gray-500">
                    <p>Decimals: {balance.decimals}</p>
                  </div>
                </div>

                {hasLocked && (
                  <div className="mt-3">
                    <div className="w-full bg-gray-200 rounded-full h-2">
                      <div
                        className="bg-orange-500 h-2 rounded-full transition-all"
                        style={{
                          width: `${(parseFloat(balance.lockedBalance) / parseFloat(balance.totalBalance)) * 100}%`
                        }}
                      />
                    </div>
                    <p className="text-xs text-gray-500 mt-1">
                      {((parseFloat(balance.lockedBalance) / parseFloat(balance.totalBalance)) * 100).toFixed(2)}% locked
                    </p>
                  </div>
                )}
              </div>
            );
          })
        )}
      </div>
    </div>
  );
}

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](https://docs.0xmonaco.com/sdk/react/hooks/balances.md#usevault) - Deposit and withdraw tokens
* [useTrade](https://docs.0xmonaco.com/sdk/react/hooks/trading.md#usetrade) - Place and manage orders that lock/unlock balances
* [WebSocket API](https://docs.0xmonaco.com/reference/websockets.md#balances) - Low-level balances WebSocket channel
* [TypeScript SDK](https://docs.0xmonaco.com/sdk/typescript/websockets.md) - Core SDK WebSocket methods

## useUserMovements

> [!NOTE]
>
> **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`](https://docs.0xmonaco.com/sdk/react/hooks/balances#useuserbalances) for the live balance change a fill produces.

In that history, `balanceBefore` / `balanceAfter` / `lockedBefore` / `lockedAfter` describe a spot balance row. A margin fill leg, a `FUNDING` row, and a deposit or withdrawal that goes straight into or out of the parent margin account 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.

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

Maximum number of movements to keep in memory

**pageSize** `number`

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`.

**entryType** `LedgerEntryType`

Filter the initial REST history by entry type. Changing this value re-fetches history. Live WebSocket events are not filtered by the hook.

**transactionType** `TransactionType`

Filter the initial REST history by transaction type. Changing this value re-fetches history. Live WebSocket events are not filtered by the hook.

**assetId** `string`

Filter the initial REST history by asset UUID. Live WebSocket events are not filtered by the hook.

### Return Values

**movements** `UserMovementEvent[]`

Array of movement events, newest first. Automatically deduplicated by movement ID.

**loading** `boolean`

`true` while fetching initial movements, `false` once loaded

**subscribed** `boolean`

`true` after the channel handler is registered, `false` otherwise. This does not report socket connection or channel delivery state.

**error** `Error | null`

Error object if something went wrong, `null` otherwise

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

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 <div>Loading movements...</div>;
  }

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

  return (
    <div>
      <p>Movement handler: {subscribed ? "Registered" : "Not registered"}</p>
      <h2>Recent Movements ({movements.length})</h2>
      {movements.map(movement => (
        <div key={movement.data.id}>
          <strong>{movement.data.transactionType}</strong>
          <span>{movement.data.amount}</span>
          <small>{movement.data.tokenAddress.slice(0, 6)}...</small>
        </div>
      ))}
    </div>
  );
}
```

### 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 (
    <div>
      <h2>Recent Deposits</h2>
      {deposits.map(deposit => (
        <div key={deposit.data.id}>
          {deposit.data.amount} deposited
          {deposit.data.txHash && (
            <a href={`https://explorer.com/tx/${deposit.data.txHash}`}>
              View TX
            </a>
          )}
        </div>
      ))}
    </div>
  );
}
```

### 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 <div>Loading...</div>;

  return (
    <div>
      {assetCredits.map(m => (
        <div key={m.data.id}>
          <strong>{m.data.amount}</strong> {m.data.symbol}
        </div>
      ))}
    </div>
  );
}
```

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

  return (
    <div>
      {Object.entries(byToken).map(([token, moves]) => (
        <div key={token}>
          <h3>{token.slice(0, 6)}...</h3>
          <p>{moves.length} movements</p>
        </div>
      ))}
    </div>
  );
}
```

### Track Order Lifecycle

```tsx theme={null}
function OrderMovements({ orderId }: { orderId: string }) {
  const { movements } = useUserMovements({ maxMovements: 100 });

  // A spot order's LOCK and UNLOCK reference the order as "Order"
  const orderMovements = movements.filter(
    m => m.data.referenceId === orderId && m.data.referenceType?.toLowerCase() === "order"
  );

  return (
    <div>
      <h3>Order Movements</h3>
      {orderMovements.map(m => (
        <div key={m.data.id}>
          <strong>{m.data.entryType}</strong>: {m.data.amount}
          <small>{m.data.description}</small>
        </div>
      ))}
    </div>
  );
}
```

### 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 <p>No movement snapshot available</p>;

  const { balanceAfter, lockedAfter } = latestMovement.data;

  return (
    <div>
      <p>Available after movement: {balanceAfter ?? "Unavailable"}</p>
      <p>Locked after movement: {lockedAfter ?? "Unavailable"}</p>
    </div>
  );
}
```

### Error Handling

```tsx theme={null}
function MovementsWithDismissibleError() {
  const { movements, loading, error, clearError } = useUserMovements({ maxMovements: 50 });

  if (error) {
    return (
      <div>
        <p>Error: {error.message}</p>
        <button onClick={clearError}>Dismiss Error</button>
      </div>
    );
  }

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

  return (
    <div>
      {movements.map(m => (
        <div key={m.data.id}>{m.data.transactionType}</div>
      ))}
    </div>
  );
}
```

### 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 (
      <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 movements</p>
        <p className="text-sm text-red-600">{error.message}</p>
      </div>
    );
  }

  return (
    <div className="space-y-4">
      {/* Handler registration status; not socket connectivity */}
      <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 ? "Movement handler registered" : "Movement handler not registered"}
        </span>
      </div>

      {/* Movements List */}
      <div className="space-y-2">
        {movements.length === 0 ? (
          <p className="text-gray-500 text-center py-8">No movements yet</p>
        ) : (
          movements.map(movement => (
            <div 
              key={movement.data.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 items-start gap-3">
                  <span className="text-2xl">{getIcon(movement.data.transactionType)}</span>
                  <div>
                    <p className="font-medium">{movement.data.transactionType}</p>
                    <p className={`text-sm ${getColor(movement.data.entryType)}`}>
                      {movement.data.entryType}: {movement.data.amount}
                    </p>
                    {movement.data.description && (
                      <p className="text-xs text-gray-500 mt-1">
                        {movement.data.description}
                      </p>
                    )}
                  </div>
                </div>
                <div className="text-right text-sm text-gray-500">
                  {movement.data.createdAt && (
                    <p>{formatDistanceToNow(new Date(movement.data.createdAt), { addSuffix: true })}</p>
                  )}
                </div>
              </div>
              
              {/* Balance Change */}
              {movement.data.balanceBefore && movement.data.balanceAfter && (
                <div className="mt-2 pt-2 border-t border-gray-100 text-xs text-gray-600">
                  Balance: {movement.data.balanceBefore} → {movement.data.balanceAfter}
                </div>
              )}
              
              {/* Transaction Hash */}
              {movement.data.txHash && (
                <a
                  href={`https://seitrace.com/tx/${movement.data.txHash}`}
                  target="_blank"
                  rel="noopener noreferrer"
                  className="mt-2 text-xs text-blue-600 hover:underline inline-block"
                >
                  View on Explorer →
                </a>
              )}
            </div>
          ))
        )}
      </div>
    </div>
  );
}

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" on a spot order's lock or released lock, "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](https://docs.0xmonaco.com/reference/websockets.md#movements) - Low-level movements WebSocket channel
* [TypeScript SDK](https://docs.0xmonaco.com/sdk/typescript/websockets.md#movements-channel) - Core SDK WebSocket methods
* [useProfile](https://docs.0xmonaco.com/sdk/react/hooks/session.md#useprofile) - User profile and balance queries

## useVault

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

const { 
  approve,
  deposit,
  withdraw,
  retryWithdrawal,
  listPendingWithdrawals,
  getAllowance,
  needsApproval
} = useVault();
```

### Vault Operations

**deposit** `(assetId, amount, autoWait?) =\> Promise\<TransactionResult\>`

Deposit tokens into vault. Use asset UUID from `sdk.profile.getUserBalances()`.

**withdraw** `(assetId, amount, autoWait?, source?) =\> Promise\<WithdrawResult\>`

Withdraw tokens from vault. Use asset UUID from `sdk.profile.getUserBalances()`. `source` is `"spot"` or `"margin"`; see [`sdk.vault.withdraw`](https://docs.0xmonaco.com/sdk/typescript/vault).

**retryWithdrawal** `(withdrawalIndex, autoWait?) =\> Promise\<WithdrawResult\>`

Retry the on-chain submission of a withdrawal that never landed. It does **not** create a new withdrawal; see [`sdk.vault.retryWithdrawal`](https://docs.0xmonaco.com/sdk/typescript/vault).

**listPendingWithdrawals** `(params?) =\> Promise\<ListPendingWithdrawalsResponse\>`

List the caller's withdrawals still awaiting on-chain confirmation, newest first, cursor-paginated with `pageSize` and `pageToken`. Wraps [`sdk.withdrawals.listPendingWithdrawals`](https://docs.0xmonaco.com/sdk/typescript/vault).

### Token Approvals

**approve** `(assetId, amount, autoWait?) =\> Promise\<TransactionResult\>`

Approve vault to spend tokens. Use asset UUID from trading pairs.

**getAllowance** `(assetId) =\> Promise\<bigint\>`

Check current token allowance by asset UUID

**needsApproval** `(assetId, amount) =\> Promise\<boolean\>`

Check if approval is needed for an asset UUID

### Validation Requirements

All vault operations include built-in validation to prevent common errors:

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 <button onClick={handleDeposit}>Deposit</button>;
}
```


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