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

# Holders Stream

> Real-time holder positions WebSocket stream — trades-based position tracking with periodic DB sync

<Tip> This endpoint is only available to Growth and Enterprise plans. </Tip>

## Real-time vs HTTP

<CardGroup>
  <Card title="GET Method" icon="link" href="/rest-api-reference/endpoint/token-holder-positions">
    **Available in HTTP queries**

    Use the same filtering system in HTTP GET requests for simple queries.
  </Card>

  <Card title="POST Method" icon="code" href="/rest-api-reference/endpoint/token-holder-positions">
    **Available in HTTP queries**

    Use the same filtering system in HTTP POST requests for advanced configurations.
  </Card>
</CardGroup>

## Overview

The Holders Stream provides real-time position tracking for token holders. It tracks holder positions directly from swap events and periodically resyncs from the database for accuracy.

### Protocol

| Message  | Description                                           |
| -------- | ----------------------------------------------------- |
| `init`   | Full snapshot of top 100 holders on subscribe         |
| `update` | Single holder position changed (balance, PnL, volume) |
| `sync`   | Periodic full resync from DB every 30s                |

## Endpoint Details

* **URL**: `wss://api.mobula.io`
* **Event Type**: `holders`

## Subscription

```json theme={null}
{
  "type": "holders",
  "authorization": "YOUR_API_KEY",
  "payload": {
    "tokens": [
      {
        "address": "So11111111111111111111111111111111111111112",
        "blockchain": "Solana"
      }
    ],
    "sortBy": "balance",
    "subscriptionTracking": true,
    "maxUpdatesPerMinute": 30
  }
}
```

### Multiple Tokens

```json theme={null}
{
  "type": "holders",
  "authorization": "YOUR_API_KEY",
  "payload": {
    "tokens": [
      {
        "address": "So11111111111111111111111111111111111111112",
        "blockchain": "Solana"
      },
      {
        "address": "0x1234567890abcdef1234567890abcdef12345678",
        "blockchain": "evm:8453"
      }
    ],
    "sortBy": "realizedPnl",
    "subscriptionTracking": true
  }
}
```

### Parameters

* **`tokens`** (required): Array of token subscription items, each containing:
  * **`address`** (required): Token address
  * **`blockchain`** (required): Blockchain identifier (e.g., `"Solana"`, `"evm:8453"`)
* **`sortBy`** (optional, default: `"balance"`): Controls how the top 100 holders are sorted. Accepts `"balance"` (sort by USD balance descending) or `"realizedPnl"` (sort by realized PnL descending). Use `"realizedPnl"` for a top traders view
* **`subscriptionId`** (optional): Unique identifier for your WebSocket connection. Auto-generated if not provided
* **`subscriptionTracking`** (optional, default: `false`): Include subscription details in response logs for debugging
* **`maxUpdatesPerMinute`** (optional): Throttle the number of `update` messages sent per minute. For example, `maxUpdatesPerMinute: 30` limits updates to at most one every 2 seconds. Useful for reducing bandwidth on high-activity tokens. When not set, updates are sent as fast as they arrive (with a minimum 100ms batching interval)

<Note>
  Maximum 10 tokens per connection for optimal performance.
</Note>

## Message Types

### `init` — Full snapshot on subscribe

Sent once when you subscribe. Contains the top 100 holders sorted by the `sortBy` criteria (balance by default, or realized PnL):

```json theme={null}
{
  "type": "init",
  "subscriptionId": "abc123",
  "data": {
    "tokenKey": "solana:mainnet|So11111111111111111111111111111111111111112",
    "holders": [
      {
        "walletAddress": "7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU",
        "tokenAddress": "So11111111111111111111111111111111111111112",
        "chainId": "solana:mainnet",
        "tokenAmount": "1000000",
        "tokenAmountRaw": "1000000000000",
        "tokenAmountUSD": "50000",
        "percentageOfTotalSupply": "0.5",
        "buys": 12,
        "sells": 3,
        "volumeBuyUSD": "60000",
        "volumeSellUSD": "10000",
        "volumeBuyToken": "1200000",
        "volumeSellToken": "200000",
        "avgBuyPriceUSD": "0.05",
        "avgSellPriceUSD": "0.06",
        "totalBoughtTokens": "1200000",
        "totalSoldTokens": "200000",
        "totalBoughtUSD": "60000",
        "totalSoldUSD": "10000",
        "realizedPnlUSD": "5000",
        "unrealizedPnlUSD": "15000",
        "totalPnlUSD": "20000",
        "isLiquidityPool": false,
        "labels": ["insider"],
        "walletFundAt": "2025-09-20T10:00:00.000Z",
        "lastActivityAt": "2025-09-24T23:25:00.000Z",
        "firstTradeAt": "2025-09-20T10:05:00.000Z",
        "lastTradeAt": "2025-09-24T23:25:00.000Z",
        "platform": {
          "id": "photon",
          "name": "Photon",
          "logo": "https://mobula.io/platforms/photon.png"
        },
        "walletMetadata": {
          "entityName": "binance",
          "entityType": "cex",
          "labels": ["CEX"]
        },
        "fundingInfo": {
          "from": "0xabc...def",
          "date": "2025-09-20T10:00:00.000Z",
          "chainId": "solana:mainnet",
          "txHash": "5xKXtg2CW87d97TXJSDpbD5...",
          "amount": "1000000000",
          "formattedAmount": 1.0,
          "currency": {
            "name": "Solana",
            "symbol": "SOL",
            "logo": null,
            "decimals": 9,
            "address": "So11111111111111111111111111111111111111112"
          },
          "fromWalletLogo": null,
          "fromWalletTag": "binance",
          "fromWalletMetadata": { "entityName": "Binance", "entityType": "exchange", "entityLogo": "https://...", "entityLabels": ["CEX"] }
        }
      }
    ]
  }
}
```

### `update` — Single holder changed

Sent in real-time when a swap changes a holder's position. Both the buyer and seller are updated:

```json theme={null}
{
  "type": "update",
  "subscriptionId": "abc123",
  "data": {
    "walletAddress": "7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU",
    "tokenAddress": "So11111111111111111111111111111111111111112",
    "chainId": "solana:mainnet",
    "tokenAmount": "800000",
    "tokenAmountRaw": "800000000000",
    "tokenAmountUSD": "40000",
    "percentageOfTotalSupply": "0.4",
    "buys": 12,
    "sells": 5,
    "volumeBuyUSD": "60000",
    "volumeSellUSD": "20000",
    "volumeBuyToken": "1200000",
    "volumeSellToken": "400000",
    "avgBuyPriceUSD": "0.05",
    "avgSellPriceUSD": "0.04",
    "totalBoughtTokens": "1200000",
    "totalSoldTokens": "400000",
    "totalBoughtUSD": "60000",
    "totalSoldUSD": "20000",
    "realizedPnlUSD": "10000",
    "unrealizedPnlUSD": "5000",
    "totalPnlUSD": "15000",
    "isLiquidityPool": false,
    "labels": ["insider"],
    "walletFundAt": null,
    "lastActivityAt": "2025-09-24T23:30:00.000Z",
    "firstTradeAt": "2025-09-20T10:05:00.000Z",
    "lastTradeAt": "2025-09-24T23:30:00.000Z",
    "platform": {
      "id": "photon",
      "name": "Photon",
      "logo": "https://mobula.io/platforms/photon.png"
    },
    "walletMetadata": null,
    "fundingInfo": {
      "from": null,
      "date": null,
      "chainId": null,
      "txHash": null,
      "amount": null,
      "formattedAmount": null,
      "currency": null,
      "fromWalletLogo": null,
      "fromWalletTag": null,
      "fromWalletMetadata": null
    }
  }
}
```

When a holder's balance reaches 0, the `update` message is sent with `tokenAmount: "0"`. The client should remove this holder from its list.

### `sync` — Periodic full resync

Sent every 30 seconds with a fresh snapshot from the database (same format as `init`). Use this to reconcile any missed updates or drift:

```json theme={null}
{
  "type": "sync",
  "subscriptionId": "abc123",
  "data": {
    "tokenKey": "solana:mainnet|So11111111111111111111111111111111111111112",
    "holders": [ ... ]
  }
}
```

## Data Model

Each holder object contains the following fields:

| Field                     | Type           | Description                                                     |
| ------------------------- | -------------- | --------------------------------------------------------------- |
| `walletAddress`           | string         | Wallet address                                                  |
| `tokenAddress`            | string         | Token contract address                                          |
| `chainId`                 | string         | Chain identifier (e.g., `solana:mainnet`, `evm:1`)              |
| `tokenAmount`             | string         | Current token balance (formatted)                               |
| `tokenAmountRaw`          | string         | Raw token balance (with decimals)                               |
| `tokenAmountUSD`          | string         | Current balance value in USD                                    |
| `percentageOfTotalSupply` | string         | Percentage of total supply held                                 |
| `buys`                    | number         | Total buy transactions                                          |
| `sells`                   | number         | Total sell transactions                                         |
| `volumeBuyUSD`            | string         | Total buy volume in USD                                         |
| `volumeSellUSD`           | string         | Total sell volume in USD                                        |
| `volumeBuyToken`          | string         | Total buy volume in token units                                 |
| `volumeSellToken`         | string         | Total sell volume in token units                                |
| `avgBuyPriceUSD`          | string         | Average buy price in USD                                        |
| `avgSellPriceUSD`         | string         | Average sell price in USD                                       |
| `totalBoughtTokens`       | string         | Total tokens bought                                             |
| `totalSoldTokens`         | string         | Total tokens sold                                               |
| `totalBoughtUSD`          | string         | Total USD spent buying                                          |
| `totalSoldUSD`            | string         | Total USD received selling                                      |
| `realizedPnlUSD`          | string         | Realized profit/loss in USD                                     |
| `unrealizedPnlUSD`        | string         | Unrealized profit/loss in USD                                   |
| `totalPnlUSD`             | string         | Total PnL (realized + unrealized)                               |
| `isLiquidityPool`         | boolean        | Whether this address is a liquidity pool                        |
| `labels`                  | string\[]      | Wallet tags (e.g., `insider`, `bundler`, `sniper`, `dev`, `LP`) |
| `walletFundAt`            | string \| null | When the wallet was first funded                                |
| `lastActivityAt`          | string \| null | Last activity timestamp                                         |
| `firstTradeAt`            | string \| null | First trade timestamp for this token                            |
| `lastTradeAt`             | string \| null | Last trade timestamp for this token                             |
| `platform`                | object \| null | Trading platform used (see below)                               |
| `walletMetadata`          | object \| null | Entity metadata from wallet labeling (see below)                |
| `fundingInfo`             | object         | Wallet funding source details (see below)                       |

### `platform`

Trading platform the wallet used (e.g., Photon, BullX, Maestro):

```json theme={null}
{
  "id": "photon",
  "name": "Photon",
  "logo": "https://mobula.io/platforms/photon.png"
}
```

### `walletMetadata`

Entity information from wallet labeling:

```json theme={null}
{
  "entityName": "binance",
  "entityType": "cex",
  "labels": ["CEX"]
}
```

### `fundingInfo`

Where the wallet's initial funds came from:

```json theme={null}
{
  "from": "0xabc...def",
  "date": "2025-09-20T10:00:00.000Z",
  "chainId": "solana:mainnet",
  "txHash": "5xKXtg2CW87d97TXJSDpbD5...",
  "amount": "1000000000",
  "formattedAmount": 1.0,
  "currency": {
    "name": "Solana",
    "symbol": "SOL",
    "logo": null,
    "decimals": 9,
    "address": "So11111111111111111111111111111111111111112"
  },
  "fromWalletLogo": null,
  "fromWalletTag": "binance",
  "fromWalletMetadata": { "entityName": "Binance", "entityType": "exchange", "entityLogo": "https://...", "entityLabels": ["CEX"] }
}
```

## Client Implementation

```typescript theme={null}
const ws = new WebSocket('wss://api.mobula.io');

ws.onopen = () => {
  ws.send(JSON.stringify({
    type: 'holders',
    authorization: 'YOUR_API_KEY',
    payload: {
      tokens: [{ address: 'So11111111111111111111111111111111111111112', blockchain: 'Solana' }],
      subscriptionTracking: true,
    },
  }));
};

let holders = new Map();

ws.onmessage = (event) => {
  const msg = JSON.parse(event.data);

  switch (msg.type) {
    case 'init':
    case 'sync':
      // Full snapshot — replace state
      holders = new Map(msg.data.holders.map(h => [h.walletAddress, h]));
      break;

    case 'update': {
      const data = msg.data;
      const balance = Number(data.tokenAmount) || 0;
      if (balance <= 0) {
        holders.delete(data.walletAddress);
      } else {
        holders.set(data.walletAddress, data);
      }
      break;
    }
  }
};
```

## Key Features

* **Trades-based tracking** — Positions are updated from swap events in real-time, not from external services
* **LP positions included** — Liquidity pool addresses are tracked and updated with `isLiquidityPool: true`
* **Both sides updated** — On each swap, both the buyer (recipient) and seller (sender) positions are updated
* **Post-balance accuracy** — Uses on-chain post-balance from the swap for exact balance tracking
* **Periodic resync** — Automatic `sync` from DB every 30s ensures consistency
* **Capped at 100** — Init and sync return top 100 holders, sorted by balance (default) or realized PnL via `sortBy`

## Connection Keepalive (Ping/Pong)

To maintain active WebSocket connections and prevent timeouts:

**Send ping:**

```json theme={null}
{"event":"ping"}
```

The server will respond with a pong message to confirm the connection is active.

<Tip>Use ping messages periodically (every 30-60 seconds) to keep long-lived connections alive.</Tip>

## Unsubscribe

### Unsubscribe from all holder streams

```json theme={null}
{
  "type": "unsubscribe",
  "payload": {
    "type": "holders"
  }
}
```

### Unsubscribe from specific subscription

```json theme={null}
{
  "type": "unsubscribe",
  "payload": {
    "subscriptionId": "abc123"
  }
}
```

## Support

Need help? Our response times are \< 1h.

<CardGroup>
  <Card title="Support" icon="Telegram" href="https://t.me/mobuladevelopers?start=Mobula_API_Support_Key">
    Telegram
  </Card>

  <Card title="Support" icon="Slack" href="https://join.slack.com/t/mobulaapi/shared_invite/zt-29zrrpjnl-I0tyD73sy7zKy8q~KLL3Ug">
    Slack
  </Card>

  <Card title="Need help?" icon="envelope" href="mailto:contact@mobulalabs.org">
    Email
  </Card>
</CardGroup>
