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

# useUserMovements

> Real-time user balance movements and transaction history

<Note>
  **New in v0.5.4** - Real-time user movement tracking
</Note>

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.

<ParamField path="maxMovements" type="number" default={50}>
  Maximum number of movements to keep in memory
</ParamField>

<ParamField path="pageSize" type="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`.
</ParamField>

<ParamField path="entryType" type="LedgerEntryType">
  Filter the initial REST history by entry type. Changing this value re-fetches history. Live WebSocket events are not filtered by the hook.
</ParamField>

<ParamField path="transactionType" type="TransactionType">
  Filter the initial REST history by transaction type. Changing this value re-fetches history. Live WebSocket events are not filtered by the hook.
</ParamField>

<ParamField path="assetId" type="string">
  Filter the initial REST history by asset UUID. Live WebSocket events are not filtered by the hook.
</ParamField>

## Return Values

<ResponseField name="movements" type="UserMovementEvent[]">
  Array of movement events, newest first. Automatically deduplicated by movement ID.
</ResponseField>

<ResponseField name="loading" type="boolean">
  `true` while fetching initial movements, `false` once loaded
</ResponseField>

<ResponseField name="subscribed" type="boolean">
  `true` after the channel handler is registered, `false` otherwise. This does not report socket connection or channel delivery state.
</ResponseField>

<ResponseField name="error" type="Error | null">
  Error object if something went wrong, `null` otherwise
</ResponseField>

<ResponseField name="clearError" type="() => void">
  Dismisses the current error. It does not retry the request or re-register the handler.
</ResponseField>

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

  // Find all movements related to this order
  const orderMovements = movements.filter(
    m => m.data.referenceId === orderId && m.data.referenceType === "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", "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


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