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

# Wallet Balance Tracking Cookbook

> How to track SOL & token balances across thousands of market-making wallets using Mobula webhooks, portfolio API, and WebSocket streams.

## Overview

<Tip>
  **Ready-to-run example**: All code snippets in this guide are available as runnable scripts in the [wallet-balance-tracker](https://github.com/MobulaFi/wallet-balance-tracker) repo.

  ```bash theme={null}
  git clone https://github.com/MobulaFi/wallet-balance-tracker.git
  cd wallet-balance-tracker
  bun install
  MOBULA_API_KEY=your_key bun run demo
  ```
</Tip>

This guide covers how to **track wallet balances** (native SOL + SPL tokens) across **thousands of wallets** — typical for market-making operations where you have many wallets trading different tokens on Solana.

There are **3 approaches**, each suited to different use cases:

| Approach                                     | Best for                                        | Latency     | Complexity |
| -------------------------------------------- | ----------------------------------------------- | ----------- | ---------- |
| **Webhooks** (transfer events)               | Real-time balance deltas via push notifications | \~1-3s      | Medium     |
| **Portfolio API** (polling)                  | On-demand balance snapshots                     | Per-request | Low        |
| **WebSocket streams** (balance subscription) | Persistent real-time balance feeds              | Sub-second  | Higher     |

**Recommendation for MM wallets**: Use **webhooks** to get notified of every transfer in/out of your wallets, then call the **portfolio API** to refresh the full balance when needed.

***

## Approach 1: Webhooks (Recommended for MM)

Webhooks let you receive **push notifications** every time tokens move in or out of your wallets. You track `transfer` events filtered to your wallet addresses.

<Note>
  Full runnable script: [`src/create-webhooks.ts`](https://github.com/MobulaFi/wallet-balance-tracker/blob/main/src/create-webhooks.ts)
</Note>

### Step 1: Create a Transfer Webhook

```typescript theme={null}
const API_KEY = 'YOUR_API_KEY';

// List your MM wallet addresses
const wallets = [
  'WaLLet1111111111111111111111111111111111111',
  'WaLLet2222222222222222222222222222222222222',
  'WaLLet3333333333333333333333333333333333333',
  // ... up to hundreds of wallets per webhook
];

// Build filter: match transfers FROM or TO any of your wallets
const filters = {
  or: [
    ...wallets.map(w => ({ eq: ['transactionFrom', w] })),
    ...wallets.map(w => ({ eq: ['transactionTo', w] })),
  ],
};

const response = await fetch('https://api.mobula.io/api/1/webhook', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    name: 'mm-wallet-tracker-solana',
    chainIds: ['solana:solana'],
    events: ['transfer'],
    apiKey: API_KEY,
    url: 'https://your-server.com/webhook/balance-updates',
    filters,
  }),
});

const webhook = await response.json();

// IMPORTANT: Save this immediately — it's only shown once!
console.log('Webhook Secret:', webhook.webhookSecret);
console.log('Stream ID:', webhook.id);
```

<Warning>
  **Filter limit**: Max **1,000 leaf operations** per webhook. With the `or` filter above, each wallet uses 2 operations (from + to), so you can track up to **500 wallets per webhook**. For more wallets, create multiple webhooks — see [Scaling to Thousands of Wallets](#scaling-to-thousands-of-wallets).
</Warning>

### Step 2: Receive Transfer Events on Your Server

Every time a transfer happens involving your wallets, Mobula POSTs a payload like this:

```json theme={null}
{
  "streamId": "d628fe5d-f550-4c8b-b1c2-c0a91d9d1cfb",
  "chainId": "solana:solana",
  "data": [
    {
      "type": "transfer",
      "transactionHash": "5abc...xyz",
      "blockNumber": 285123456,
      "transactionFrom": "WaLLet1111111111111111111111111111111111111",
      "transactionTo": "SomeOtherAddress...",
      "contract": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
      "from": "WaLLet1111111111111111111111111111111111111",
      "to": "SomeOtherAddress...",
      "amount": "1000000",
      "amountUSD": "1.00",
      "date": "2026-03-20T10:00:00.000Z"
    }
  ]
}
```

**Key transfer fields:**

| Field                               | Description                                      |
| ----------------------------------- | ------------------------------------------------ |
| `from` / `to`                       | Actual token sender/receiver                     |
| `transactionFrom` / `transactionTo` | Transaction-level initiator/target               |
| `contract`                          | SPL token mint address (token being transferred) |
| `amount`                            | Raw token amount (needs decimal adjustment)      |
| `amountUSD`                         | USD value of the transfer                        |

### Step 3: Build Your Webhook Server

<Note>
  Full runnable server with health check & balance query endpoints: [`src/server.ts`](https://github.com/MobulaFi/wallet-balance-tracker/blob/main/src/server.ts)

  ```bash theme={null}
  WALLETS=addr1,addr2 WEBHOOK_SECRET=whsec_xxx PORT=3000 bun run server
  ```
</Note>

```typescript theme={null}
import { createHmac } from 'node:crypto';

const WEBHOOK_SECRET = process.env.WEBHOOK_SECRET; // whsec_...
const WALLET_SET = new Set(YOUR_WALLETS);

// Verify Mobula webhook signature
function verifySignature(rawBody: string, signature: string): boolean {
  const expected = createHmac('sha256', WEBHOOK_SECRET)
    .update(rawBody)
    .digest('hex');
  return signature === `sha256=${expected}`;
}

// Process incoming transfer events
function handleWebhook(rawBody: string, headers: Headers) {
  // 1. Verify signature
  const signature = headers.get('x-signature');
  if (!verifySignature(rawBody, signature)) {
    throw new Error('Invalid signature');
  }

  // 2. Replay protection (5 min window)
  const timestamp = Number(headers.get('x-timestamp'));
  if (Math.abs(Date.now() / 1000 - timestamp) > 300) {
    throw new Error('Request too old');
  }

  // 3. Process transfers
  const payload = JSON.parse(rawBody);
  for (const event of payload.data) {
    if (event.type !== 'transfer') continue;

    if (WALLET_SET.has(event.from)) {
      console.log(`[OUT] ${event.from} sent ${event.amount} of ${event.contract}`);
    }
    if (WALLET_SET.has(event.to)) {
      console.log(`[IN] ${event.to} received ${event.amount} of ${event.contract}`);
    }
  }
}
```

### Step 4: Also Track Swap Events (Optional)

If your MM wallets are actively trading, you can also track **swap events** to see trades:

```typescript theme={null}
const swapFilters = {
  or: wallets.map(w => ({ eq: ['swapSenderAddress', w] })),
};

await fetch('https://api.mobula.io/api/1/webhook', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    name: 'mm-swap-tracker-solana',
    chainIds: ['solana:solana'],
    events: ['swap'],
    apiKey: API_KEY,
    url: 'https://your-server.com/webhook/swap-updates',
    filters: swapFilters,
  }),
});
```

**Swap payload fields:**

| Field                                 | Description                                      |
| ------------------------------------- | ------------------------------------------------ |
| `swapSenderAddress`                   | Wallet that performed the swap                   |
| `poolAddress`                         | DEX pool used                                    |
| `poolType`                            | DEX protocol (raydium, raydium-clmm, orca, etc.) |
| `addressToken0` / `addressToken1`     | Tokens in the pair                               |
| `amount0` / `amount1`                 | Swap amounts                                     |
| `amountUSD`                           | Total swap value in USD                          |
| `rawPostBalance0` / `rawPostBalance1` | Wallet balance AFTER the swap                    |

<Tip>
  **Pro tip**: The `rawPostBalance0` and `rawPostBalance1` fields in swap events give you the wallet's balance of both tokens **after** the swap — no extra API call needed!
</Tip>

***

## Approach 2: Portfolio API (Polling)

For **on-demand balance checks** or to **initialize/reconcile** your balance tracking, use the Portfolio API.

<Note>
  Full runnable script with batch polling & rate limiting: [`src/poll-balances.ts`](https://github.com/MobulaFi/wallet-balance-tracker/blob/main/src/poll-balances.ts)

  ```bash theme={null}
  MOBULA_API_KEY=xxx WALLETS=addr1,addr2 bun run poll-balances
  ```
</Note>

### Single Wallet Balance

```typescript theme={null}
// Get all token balances for a single wallet
const response = await fetch(
  'https://api.mobula.io/api/2/wallet/token-balances?' + new URLSearchParams({
    wallet: 'WaLLet1111111111111111111111111111111111111',
    chainIds: 'solana:solana',
  }),
  { headers: { Authorization: `Bearer ${API_KEY}` } }
);

const { data } = await response.json();

// data = [
//   {
//     token: {
//       address: "So11111111111111111111111111111111111111112",
//       name: "SOL", symbol: "SOL", decimals: 9,
//     },
//     balance: 12.5,            // Human-readable (decimal-adjusted)
//     rawBalance: "12500000000" // Raw lamports
//   },
//   {
//     token: {
//       address: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
//       name: "USD Coin", symbol: "USDC", decimals: 6,
//     },
//     balance: 1500.25,
//     rawBalance: "1500250000"
//   }
// ]
```

### Full Portfolio with USD Values

```typescript theme={null}
const response = await fetch(
  'https://api.mobula.io/api/1/wallet/portfolio?' + new URLSearchParams({
    wallet: 'WaLLet1111111111111111111111111111111111111',
    chains: 'solana:solana',
  }),
  { headers: { Authorization: `Bearer ${API_KEY}` } }
);

const { data } = await response.json();
// data.total_wallet_balance = 25000.50 (total USD)
// data.assets = [{ asset: { name, symbol }, price, token_balance, estimated_balance, ... }]
```

### Batch Polling for Thousands of Wallets

```typescript theme={null}
async function pollAllWallets(wallets: string[], apiKey: string) {
  const BATCH_SIZE = 10;
  const DELAY_MS = 200;
  const results = new Map();

  for (let i = 0; i < wallets.length; i += BATCH_SIZE) {
    const batch = wallets.slice(i, i + BATCH_SIZE);

    await Promise.all(batch.map(async (wallet) => {
      const res = await fetch(
        'https://api.mobula.io/api/2/wallet/token-balances?' + new URLSearchParams({
          wallet, chainIds: 'solana:solana',
        }),
        { headers: { Authorization: `Bearer ${apiKey}` } }
      );
      const data = await res.json();
      results.set(wallet, data.data);
    }));

    if (i + BATCH_SIZE < wallets.length) {
      await new Promise(r => setTimeout(r, DELAY_MS));
    }
  }
  return results;
}
```

***

## Approach 3: WebSocket Balance Stream

For **persistent, real-time** balance feeds with the lowest latency.

<Note>
  Full runnable script with auto-reconnect: [`src/ws-stream.ts`](https://github.com/MobulaFi/wallet-balance-tracker/blob/main/src/ws-stream.ts)

  ```bash theme={null}
  MOBULA_API_KEY=xxx WALLETS=addr1,addr2 TOKENS=native,EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v bun run ws-stream
  ```
</Note>

### Subscribe to Balance Updates

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

ws.addEventListener('open', () => {
  ws.send(JSON.stringify({
    type: 'balance',
    payload: {
      items: [
        { wallet: 'YourWallet...', token: 'native', blockchain: 'solana:solana' },
        { wallet: 'YourWallet...', token: 'EPjFWdd5...', blockchain: 'solana:solana' },
      ],
      subscriptionTracking: true,
    },
    authorization: API_KEY,
  }));
});

ws.addEventListener('message', (event) => {
  const data = JSON.parse(event.data);
  // {
  //   wallet, token, chainId, balance, rawBalance,
  //   decimals, symbol, name,
  //   previousBalance, previousRawBalance  // delta info
  // }
  console.log(`${data.wallet} ${data.symbol}: ${data.balance}`);
});
```

<Warning>
  WebSocket subscriptions require specifying each **wallet + token + blockchain** triple individually. For thousands of wallets each holding many tokens, the webhook approach is more practical.
</Warning>

***

## Scaling to Thousands of Wallets

<Note>
  All scaling scripts are in the [wallet-balance-tracker](https://github.com/MobulaFi/wallet-balance-tracker) repo:

  * [`src/create-webhooks.ts`](https://github.com/MobulaFi/wallet-balance-tracker/blob/main/src/create-webhooks.ts) — auto-partitions wallets across webhooks
  * [`src/update-webhooks.ts`](https://github.com/MobulaFi/wallet-balance-tracker/blob/main/src/update-webhooks.ts) — add/remove wallets dynamically
  * [`src/poll-balances.ts`](https://github.com/MobulaFi/wallet-balance-tracker/blob/main/src/poll-balances.ts) — batch polling with rate limiting
</Note>

### Architecture

```
┌─────────────────────────────────────────────────────────┐
│                   Your MM System                        │
│                                                         │
│  ┌──────────────┐    ┌──────────────┐                   │
│  │ Wallet DB    │    │ Balance DB   │                   │
│  │ (wallets +   │    │ (wallet →    │                   │
│  │  tokens)     │    │  token →     │                   │
│  └──────┬───────┘    │  balance)    │                   │
│         │            └──────▲───────┘                   │
│         │                   │                           │
│  ┌──────▼───────┐    ┌──────┴───────┐                   │
│  │ Webhook      │    │ Webhook      │                   │
│  │ Manager      │    │ Receiver     │                   │
│  │ (CRUD)       │    │ (process     │                   │
│  └──────┬───────┘    │  events)     │                   │
│         │            └──────▲───────┘                   │
└─────────┼───────────────────┼───────────────────────────┘
          │                   │
          ▼                   │
   Mobula Webhook API    Mobula pushes
   (create/update)       transfer events
```

### 1. Partition Wallets Across Multiple Webhooks

Each webhook supports \~450 wallets (1,000 filter ops limit, 2 per wallet). The [`create-webhooks.ts`](https://github.com/MobulaFi/wallet-balance-tracker/blob/main/src/create-webhooks.ts) script handles this automatically:

```bash theme={null}
MOBULA_API_KEY=xxx \
WEBHOOK_URL=https://your-server.com/webhook/transfers \
WALLETS=addr1,addr2,...,addr2000 \
bun run create-webhooks

# Output:
# Webhook 0: 450 wallets | Stream ID: abc-123
# Webhook 1: 450 wallets | Stream ID: def-456
# Webhook 2: 450 wallets | Stream ID: ghi-789
# Webhook 3: 450 wallets | Stream ID: jkl-012
# Webhook 4: 200 wallets | Stream ID: mno-345
```

### 2. Add/Remove Wallets Dynamically

When you spin up new MM wallets or retire old ones, **update** the webhook filter with [`update-webhooks.ts`](https://github.com/MobulaFi/wallet-balance-tracker/blob/main/src/update-webhooks.ts):

```bash theme={null}
# Merge new wallets into existing webhook
MOBULA_API_KEY=xxx STREAM_ID=abc-123 MODE=merge \
WALLETS=newAddr1,newAddr2 bun run src/update-webhooks.ts

# Replace all wallets on a webhook
MOBULA_API_KEY=xxx STREAM_ID=abc-123 MODE=replace \
WALLETS=addr1,addr2,addr3 bun run src/update-webhooks.ts
```

Under the hood, this calls `PATCH /api/1/webhook`:

```typescript theme={null}
await fetch('https://api.mobula.io/api/1/webhook', {
  method: 'PATCH',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    streamId: 'YOUR_STREAM_ID',
    apiKey: 'YOUR_API_KEY',
    mode: 'merge', // or 'replace'
    filters: {
      or: [
        ...newWallets.map(w => ({ eq: ['from', w] })),
        ...newWallets.map(w => ({ eq: ['to', w] })),
      ],
    },
  }),
});
```

### 3. Initialize Balances on Startup

Before relying on webhooks for deltas, fetch current balances for all wallets using the batch poller:

```bash theme={null}
MOBULA_API_KEY=xxx WALLETS=addr1,addr2,...,addr2000 bun run poll-balances
```

### 4. Periodic Reconciliation

Webhooks can occasionally miss events (network issues, dead webhook recovery). Run periodic reconciliation by re-polling a random subset:

```typescript theme={null}
// Every 5 minutes, reconcile 50 random wallets
setInterval(async () => {
  const subset = getRandomSubset(allWallets, 50);
  for (const wallet of subset) {
    const apiBalances = await fetchWalletBalances(wallet);
    const localBalances = balanceStore.get(wallet);
    // Compare and update mismatches
  }
}, 5 * 60 * 1000);
```

***

## Recommended Production Setup

1. **Initialize**: Poll all wallets on startup (`bun run poll-balances`)
2. **Track**: Webhooks push transfer events in real-time (`bun run server`)
3. **Reconcile**: Every 5 min, re-poll a random subset of wallets
4. **Store**: Use Redis or a database instead of in-memory maps
5. **Monitor**: Track webhook delivery failures via the [list webhooks endpoint](#quick-reference)

See the complete working setup in the [wallet-balance-tracker](https://github.com/MobulaFi/wallet-balance-tracker) repo — clone it, set your env vars, and run.

***

## Quick Reference

### API Endpoints

| Action             | Method   | URL                                                 |
| ------------------ | -------- | --------------------------------------------------- |
| Create webhook     | `POST`   | `https://api.mobula.io/api/1/webhook`               |
| List webhooks      | `GET`    | `https://api.mobula.io/api/1/webhook?apiKey=...`    |
| Update webhook     | `PATCH`  | `https://api.mobula.io/api/1/webhook`               |
| Delete webhook     | `DELETE` | `https://api.mobula.io/api/1/webhook/:id`           |
| Get token balances | `GET`    | `https://api.mobula.io/api/2/wallet/token-balances` |
| Get portfolio      | `GET`    | `https://api.mobula.io/api/1/wallet/portfolio`      |

### Transfer Event Fields for Filtering

| Field             | Type   | Description            |
| ----------------- | ------ | ---------------------- |
| `from`            | string | Token sender address   |
| `to`              | string | Token receiver address |
| `transactionFrom` | string | Transaction initiator  |
| `transactionTo`   | string | Transaction target     |
| `contract`        | string | SPL token mint address |
| `amount`          | string | Raw token amount       |
| `amountUSD`       | string | USD value              |
| `blockNumber`     | number | Block number           |

### Filter Operators

| Operator     | Example                                      |
| ------------ | -------------------------------------------- |
| `eq`         | `{ "eq": ["from", "WaLLet..."] }`            |
| `neq`        | `{ "neq": ["contract", "native"] }`          |
| `gt` / `gte` | `{ "gte": ["amountUSD", "100"] }`            |
| `lt` / `lte` | `{ "lt": ["amount", "1000000"] }`            |
| `in`         | `{ "in": ["contract", ["mint1", "mint2"]] }` |
| `and`        | `{ "and": [filter1, filter2] }`              |
| `or`         | `{ "or": [filter1, filter2] }`               |

### Solana-Specific Notes

* **Native SOL address**: `So11111111111111111111111111111111111111112` (Wrapped SOL mint)
* **Chain ID**: `solana:solana`
* **SOL decimals**: 9 (1 SOL = 1,000,000,000 lamports)
* **USDC decimals**: 6
* Webhook event delivery is **batched** (up to 10 events per POST, 2-second debounce)
* Failed deliveries are **retried** every 10 minutes for up to 7 days

### Repo Scripts

| Script | Command                          | Description                                    |
| ------ | -------------------------------- | ---------------------------------------------- |
| Demo   | `bun run demo`                   | Validate API connection, fetch sample balances |
| Poll   | `bun run poll-balances`          | Fetch current balances for all wallets         |
| Create | `bun run create-webhooks`        | Create transfer webhooks (auto-partitioned)    |
| Update | `bun run src/update-webhooks.ts` | Add/remove wallets on existing webhooks        |
| Server | `bun run server`                 | Start webhook receiver server                  |
| Stream | `bun run ws-stream`              | Real-time WebSocket balance feed               |

***

<CardGroup>
  <Card title="GitHub Repo" icon="github" href="https://github.com/MobulaFi/wallet-balance-tracker">
    Ready-to-run code example
  </Card>

  <Card title="Webhook Getting Started" icon="webhook" href="/indexing-stream/stream/webhook/getting-started">
    Webhook basics
  </Card>

  <Card title="Filter Reference" icon="filter" href="/indexing-stream/stream/filters">
    Full filter documentation
  </Card>

  <Card title="Transfer Data Model" icon="arrow-right-arrow-left" href="/indexing-stream/stream/data-model/evm-data-model#transfer-model">
    Transfer event fields
  </Card>

  <Card title="Mobula SDK" icon="code" href="https://github.com/MobulaFi/mobula_sdk">
    TypeScript SDK
  </Card>

  <Card title="Support" icon="telegram" href="https://t.me/mobuladevelopers">
    Telegram support
  </Card>
</CardGroup>
