Skip to main content
This endpoint is only available to Growth and Enterprise plans.

Real-time vs HTTP

GET Method

Available in HTTP queriesUse the same filtering system in HTTP GET requests for simple queries.

POST Method

Available in HTTP queriesUse the same filtering system in HTTP POST requests for advanced configurations.

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

MessageDescription
initFull snapshot of top 100 holders on subscribe
updateSingle holder position changed (balance, PnL, volume)
syncPeriodic full resync from DB every 30s

Endpoint Details

  • URL: wss://api.mobula.io
  • Event Type: holders

Subscription

{
  "type": "holders",
  "authorization": "YOUR_API_KEY",
  "payload": {
    "tokens": [
      {
        "address": "So11111111111111111111111111111111111111112",
        "blockchain": "Solana"
      }
    ],
    "sortBy": "balance",
    "subscriptionTracking": true
  }
}

Multiple Tokens

{
  "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
Maximum 10 tokens per connection for optimal performance.

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):
{
  "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"
        }
      }
    ]
  }
}

update — Single holder changed

Sent in real-time when a swap changes a holder’s position. Both the buyer and seller are updated:
{
  "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
    }
  }
}
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:
{
  "type": "sync",
  "subscriptionId": "abc123",
  "data": {
    "tokenKey": "solana:mainnet|So11111111111111111111111111111111111111112",
    "holders": [ ... ]
  }
}

Data Model

Each holder object contains the following fields:
FieldTypeDescription
walletAddressstringWallet address
tokenAddressstringToken contract address
chainIdstringChain identifier (e.g., solana:mainnet, evm:1)
tokenAmountstringCurrent token balance (formatted)
tokenAmountRawstringRaw token balance (with decimals)
tokenAmountUSDstringCurrent balance value in USD
percentageOfTotalSupplystringPercentage of total supply held
buysnumberTotal buy transactions
sellsnumberTotal sell transactions
volumeBuyUSDstringTotal buy volume in USD
volumeSellUSDstringTotal sell volume in USD
volumeBuyTokenstringTotal buy volume in token units
volumeSellTokenstringTotal sell volume in token units
avgBuyPriceUSDstringAverage buy price in USD
avgSellPriceUSDstringAverage sell price in USD
totalBoughtTokensstringTotal tokens bought
totalSoldTokensstringTotal tokens sold
totalBoughtUSDstringTotal USD spent buying
totalSoldUSDstringTotal USD received selling
realizedPnlUSDstringRealized profit/loss in USD
unrealizedPnlUSDstringUnrealized profit/loss in USD
totalPnlUSDstringTotal PnL (realized + unrealized)
isLiquidityPoolbooleanWhether this address is a liquidity pool
labelsstring[]Wallet tags (e.g., insider, bundler, sniper, dev, LP)
walletFundAtstring | nullWhen the wallet was first funded
lastActivityAtstring | nullLast activity timestamp
firstTradeAtstring | nullFirst trade timestamp for this token
lastTradeAtstring | nullLast trade timestamp for this token
platformobject | nullTrading platform used (see below)
walletMetadataobject | nullEntity metadata from wallet labeling (see below)
fundingInfoobjectWallet funding source details (see below)

platform

Trading platform the wallet used (e.g., Photon, BullX, Maestro):
{
  "id": "photon",
  "name": "Photon",
  "logo": "https://mobula.io/platforms/photon.png"
}

walletMetadata

Entity information from wallet labeling:
{
  "entityName": "binance",
  "entityType": "cex",
  "labels": ["CEX"]
}

fundingInfo

Where the wallet’s initial funds came from:
{
  "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"
}

Client Implementation

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:
{"event":"ping"}
The server will respond with a pong message to confirm the connection is active.
Use ping messages periodically (every 30-60 seconds) to keep long-lived connections alive.

Unsubscribe

Unsubscribe from all holder streams

{
  "type": "unsubscribe",
  "payload": {
    "type": "holders"
  }
}

Unsubscribe from specific subscription

{
  "type": "unsubscribe",
  "payload": {
    "subscriptionId": "abc123"
  }
}

Support

Need help? Our response times are < 1h.

Support

Telegram

Support

Slack

Need help?

Email