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

# Pulse WebSocket Integration with the Mobula SDK

> Learn how to connect to Mobula's Pulse V2 WebSocket stream using the @mobula/sdk. This guide covers real-time token discovery, subscription management, filtering, pause/resume, and performance patterns — with open-source examples from the MTT codebase.

Mobula's Pulse V2 WebSocket stream delivers real-time token data — new pairs, bonding curve progress, and migrations — directly to your application. This guide shows you how to integrate the Pulse WSS using the `@mobula/sdk`, with practical patterns taken from the open-source [MTT (Mobula Trader Terminal)](https://github.com/MobulaFi/MTT) codebase.

<Tip>
  **Live Demo**: See Pulse in action at [mtt.gg](https://mtt.gg)

  **Source Code**: Full implementation at [github.com/MobulaFi/MTT](https://github.com/MobulaFi/MTT)
</Tip>

***

## Prerequisites

* Node.js 18+ or Bun
* A Mobula API key (Growth or Enterprise plan) — get one at [admin.mobula.io](https://admin.mobula.io)
* `@mobula/sdk` installed in your project

```bash theme={null}
npm install @mobula/sdk
# or
bun add @mobula/sdk
```

***

## Quick Start

Connect to Pulse V2 and start receiving real-time token data in under 10 lines:

```typescript theme={null}
import { MobulaClient } from '@mobula/sdk';

const client = new MobulaClient({
  restUrl: 'https://pulse-v2-api.mobula.io',
  apiKey: 'YOUR_API_KEY',
});

// Subscribe to the pulse-v2 stream
const subscriptionId = client.streams.subscribe(
  'pulse-v2',
  {
    assetMode: true,
    views: [
      { name: 'new', model: 'new', chainId: ['solana:solana'], limit: 30 },
      { name: 'bonding', model: 'bonding', chainId: ['solana:solana'], limit: 30 },
      { name: 'bonded', model: 'bonded', chainId: ['solana:solana'], limit: 30 },
    ],
  },
  (data) => {
    console.log('Received:', data);
  }
);
```

That's it — you're now receiving live token updates.

***

## How the SDK StreamClient Works

The `MobulaClient` exposes a `streams` property — an instance of `StreamClient` that manages WebSocket connections. Key behaviors:

| Feature            | Detail                                         |
| ------------------ | ---------------------------------------------- |
| **Auto-reconnect** | Exponential backoff up to 30 seconds           |
| **Heartbeat**      | Pings every 30s to keep connections alive      |
| **Deduplication**  | Subscriptions are hashed to prevent duplicates |
| **Compression**    | Per-message deflate supported                  |
| **Queue**          | Messages queued until the WebSocket is ready   |

### Core Methods

```typescript theme={null}
// Subscribe — returns a subscription ID
const id = client.streams.subscribe('pulse-v2', payload, callback);

// Unsubscribe by ID
await client.streams.unsubscribe('pulse-v2', id);

// Close the WebSocket connection entirely
client.streams.close('pulse-v2');
```

***

## Understanding the Payload

The subscription payload controls what data you receive. Here's the full shape:

```typescript theme={null}
interface PulsePayload {
  assetMode?: boolean;       // true = token mode, false = pool mode
  compressed?: boolean;      // gzip-compressed responses
  views: Array<{
    name: string;            // Your label for this view
    model: 'new' | 'bonding' | 'bonded';  // Token lifecycle stage
    chainId?: string[];      // e.g. ['solana:solana', 'evm:56']
    poolTypes?: string[];    // Filter by pool type
    sortBy?: string;         // Sort field
    sortOrder?: 'asc' | 'desc';
    limit?: number;          // Max tokens per view (max 100, default 30)
    offset?: number;         // Pagination offset
    filters?: Record<string, unknown>;  // Advanced filters (see below)
    min_socials?: number;    // 1-3, require social links
  }>;
}
```

### View Models

| Model     | Description                                                   |
| --------- | ------------------------------------------------------------- |
| `new`     | Freshly created tokens still in their bonding phase           |
| `bonding` | Tokens nearing the end of their bonding curve (final stretch) |
| `bonded`  | Tokens that have migrated to a DEX (graduated)                |

***

## Message Types

The stream sends three message types:

### `init` — Initial Data Snapshot

Sent immediately after subscribing. Contains the current state for all views:

```typescript theme={null}
{
  type: 'init',
  payload: {
    new: { data: PulseToken[] },
    bonding: { data: PulseToken[] },
    bonded: { data: PulseToken[] },
  }
}
```

### `update-token` — Token Data Changed

Sent when a token's metrics update (price, holders, market cap, etc.):

```typescript theme={null}
{
  type: 'update-token',
  payload: {
    viewName: 'new' | 'bonding' | 'bonded',
    token: PulseToken,
  }
}
```

### `new-token` — New Token Appeared

Sent when a token enters a view for the first time:

```typescript theme={null}
{
  type: 'new-token',
  payload: {
    viewName: 'new' | 'bonding' | 'bonded',
    token: PulseToken,
  }
}
```

***

## REST + WebSocket Pattern

MTT uses a **REST-first, WebSocket-second** pattern for the best user experience: load data instantly via REST, then switch to WebSocket for live updates.

Here's a simplified version of how MTT does it (`apps/mtt/src/features/pulse/hooks/usePulseV2.ts`):

```typescript theme={null}
import { MobulaClient } from '@mobula/sdk';

const client = new MobulaClient({
  restUrl: 'https://pulse-v2-api.mobula.io',
  apiKey: 'YOUR_API_KEY',
});

// 1. Load initial data via REST (fast first paint)
const initialData = await client.fetchPulseV2({
  assetMode: true,
  compressed: false,
  views: [
    { name: 'new', model: 'new', chainId: ['solana:solana'], limit: 50 },
    { name: 'bonding', model: 'bonding', chainId: ['solana:solana'], limit: 50 },
    { name: 'bonded', model: 'bonded', chainId: ['solana:solana'], limit: 50 },
  ],
});

// Render initialData immediately...

// 2. Then subscribe to WebSocket for live updates
const subscriptionId = client.streams.subscribe(
  'pulse-v2',
  {
    assetMode: true,
    views: [
      { name: 'new', model: 'new', chainId: ['solana:solana'], limit: 50 },
      { name: 'bonding', model: 'bonding', chainId: ['solana:solana'], limit: 50 },
      { name: 'bonded', model: 'bonded', chainId: ['solana:solana'], limit: 50 },
    ],
  },
  (msg) => {
    if (msg.type === 'init') {
      // Replace all view data
    }
    if (msg.type === 'update-token' || msg.type === 'new-token') {
      // Merge single token into the right view
    }
  }
);
```

<Note>
  The REST and WebSocket APIs accept the same payload shape, so you can reuse your view configuration for both.
</Note>

***

## How MTT Implements Pulse WSS

MTT is Mobula's open-source trading terminal. Its Pulse implementation is a production reference you can study and adapt. Here are the key architectural pieces:

### Architecture

```
┌──────────────────────────────────────────────────────┐
│                 PulseStreamProvider                   │
│  React Context — shares stream state with all children│
├──────────────────────────────────────────────────────┤
│                   usePulseV2 Hook                     │
│  REST initial load → WebSocket subscription           │
│  Message batching → Pause/Resume → Filter resubscribe │
├─────────────────────┬────────────────────────────────┤
│  usePulseDataStore  │     usePulseFilterStore         │
│  Zustand store:     │     Zustand + localStorage:     │
│  tokens per view    │     chain, protocol, metric     │
│  merge / sort / cap │     and audit filters           │
├─────────────────────┴────────────────────────────────┤
│                    UI Components                      │
│  TokenSection → TokenCard → FilterModal               │
└──────────────────────────────────────────────────────┘
```

### Key Files in the MTT Codebase

| File                                                   | Purpose                                                |
| ------------------------------------------------------ | ------------------------------------------------------ |
| `src/lib/sdkClient.ts`                                 | SDK wrapper with server/client mode routing            |
| `src/features/pulse/hooks/usePulseV2.ts`               | Core hook — REST load, WSS subscribe, message handling |
| `src/features/pulse/store/usePulseDataStore.ts`        | Token state management per view                        |
| `src/features/pulse/store/usePulseModalFilterStore.ts` | Filter state with localStorage persistence             |
| `src/features/pulse/context/PulseStreamContext.tsx`    | React context provider for stream state                |
| `src/features/pulse/components/FilterModal.tsx`        | Filter UI that triggers resubscription                 |
| `src/config/endpoints.ts`                              | WSS and REST URL configuration                         |

### SDK Client Wrapper

MTT wraps the SDK to support both server-side (SSR) and client-side modes. Here's the stream subscription pattern from `sdkClient.ts`:

```typescript theme={null}
import { MobulaClient } from '@mobula/sdk';
import type { PulsePayloadParams } from '@mobula/types';

type StreamSubscription = { unsubscribe: () => void };

function subscribePulseV2(
  params: PulsePayloadParams,
  callback: (data: unknown) => void
): StreamSubscription {
  const client = getMobulaClient(); // your singleton
  const subscriptionId = client.streams.subscribe('pulse-v2', params, callback);

  return {
    unsubscribe: () => {
      client.streams.unsubscribe('pulse-v2', subscriptionId);
    },
  };
}
```

### Update Batching for 60fps

MTT batches incoming WebSocket messages using `requestAnimationFrame` to avoid render storms:

```typescript theme={null}
class UpdateBatcher<T> {
  private updates: T[] = [];
  private scheduled = false;

  constructor(private callback: (updates: T[]) => void) {}

  add(update: T) {
    this.updates.push(update);
    if (!this.scheduled) {
      this.scheduled = true;
      requestAnimationFrame(() => {
        this.callback(this.updates);
        this.updates = [];
        this.scheduled = false;
      });
    }
  }
}

// Usage in the pulse hook
const batcher = new UpdateBatcher<{ view: string; token: PulseToken }>((updates) => {
  updates.forEach(({ view, token }) => {
    dataStore.mergeToken(view, token);
  });
});

// In the WebSocket callback:
client.streams.subscribe('pulse-v2', payload, (msg) => {
  if (msg.type === 'update-token' || msg.type === 'new-token') {
    batcher.add({ view: msg.payload.viewName, token: msg.payload.token });
  }
});
```

<Tip>
  Without batching, high-frequency token updates can cause hundreds of React re-renders per second. The `UpdateBatcher` coalesces them into a single render per animation frame.
</Tip>

***

## Pause and Resume

The SDK supports pausing specific views without disconnecting:

```typescript theme={null}
// Pause views (stop receiving updates for these views)
client.streams.subscribe('pulse-v2-pause', {
  action: 'pause',
  views: ['new', 'bonding'],
});

// Resume views
client.streams.subscribe('pulse-v2-pause', {
  action: 'unpause',
  views: ['new', 'bonding'],
});
```

MTT uses this when the user scrolls away from a section or when applying filters (to prevent flickering during resubscription):

```typescript theme={null}
// From usePulseV2.ts — pause during filter changes
const applyFilters = () => {
  setIsPaused(true);
  // Unsubscribe old → resubscribe with new filters
  // Resume after a short delay
  setTimeout(() => setIsPaused(false), 500);
};
```

***

## Filtering

Pass filters inside each view to narrow down the token stream. Filters use `{ gte, lte }` range syntax:

```typescript theme={null}
client.streams.subscribe('pulse-v2', {
  assetMode: true,
  views: [
    {
      name: 'new',
      model: 'new',
      chainId: ['solana:solana'],
      limit: 50,
      filters: {
        market_cap: { gte: 10000, lte: 1000000 },
        holders_count: { gte: 50 },
        volume_24h: { gte: 5000 },
        dev_holdings_percentage: { lte: 10 },
        top10_holders_percent: { lte: 50 },
      },
    },
  ],
});
```

### Available Filter Fields

| Filter                         | Type               | Description                          |
| ------------------------------ | ------------------ | ------------------------------------ |
| `market_cap`                   | `{ gte?, lte? }`   | Market capitalization in USD         |
| `volume_24h`                   | `{ gte?, lte? }`   | 24-hour trading volume               |
| `liquidity`                    | `{ gte?, lte? }`   | Available liquidity                  |
| `holders_count`                | `{ gte?, lte? }`   | Number of token holders              |
| `top10_holders_percent`        | `{ gte?, lte? }`   | Top 10 holders concentration         |
| `dev_holdings_percentage`      | `{ gte?, lte? }`   | Developer holdings %                 |
| `snipers_holdings_percentage`  | `{ gte?, lte? }`   | Sniper wallet holdings %             |
| `insiders_holdings_percentage` | `{ gte?, lte? }`   | Insider holdings %                   |
| `bundlers_holdings_percentage` | `{ gte?, lte? }`   | Bundler holdings %                   |
| `bonding_percentage`           | `{ gte?, lte? }`   | Bonding curve progress %             |
| `pro_traders_count`            | `{ gte?, lte? }`   | Number of pro traders                |
| `trades_24h`                   | `{ gte?, lte? }`   | 24h transaction count                |
| `buys_24h`                     | `{ gte?, lte? }`   | 24h buy count                        |
| `sells_24h`                    | `{ gte?, lte? }`   | 24h sell count                       |
| `fees_paid_24h`                | `{ gte?, lte? }`   | Fees paid in 24h                     |
| `created_at_offset`            | `{ gte?, lte? }`   | Token age in seconds                 |
| `deployer_migrations_count`    | `{ gte?, lte? }`   | Deployer's migration count           |
| `dexscreener_ad_paid`          | `{ equals: true }` | Has DEX Screener ad                  |
| `twitter`                      | `{ not: null }`    | Has Twitter link                     |
| `website`                      | `{ not: null }`    | Has website                          |
| `telegram`                     | `{ not: null }`    | Has Telegram link                    |
| `min_socials`                  | `number`           | Min social links (1-3)               |
| `includeKeywords`              | `string[]`         | Token name must contain one of these |
| `excludeKeywords`              | `string[]`         | Token name must not contain these    |

<Tip>
  For the complete filter reference with examples, see [Filter Details](/indexing-stream/stream/websocket/filters-details).
</Tip>

***

## Multi-Chain Support

Subscribe to multiple chains in a single view:

```typescript theme={null}
client.streams.subscribe('pulse-v2', {
  assetMode: true,
  views: [
    {
      name: 'new-multichain',
      model: 'new',
      chainId: ['solana:solana', 'evm:56', 'evm:8453', 'evm:1'],
      limit: 50,
    },
  ],
});
```

Or use separate views per chain for independent control:

```typescript theme={null}
client.streams.subscribe('pulse-v2', {
  assetMode: true,
  views: [
    { name: 'new-sol', model: 'new', chainId: ['solana:solana'], limit: 30 },
    { name: 'new-bsc', model: 'new', chainId: ['evm:56'], limit: 30 },
    { name: 'new-base', model: 'new', chainId: ['evm:8453'], limit: 30 },
  ],
});
```

***

## Custom WSS URLs

The SDK lets you override WebSocket endpoints per stream type:

```typescript theme={null}
const client = new MobulaClient({
  restUrl: 'https://pulse-v2-api.mobula.io',
  apiKey: 'YOUR_API_KEY',
  wsUrlMap: {
    'pulse-v2': 'wss://pulse-v2-api.mobula.io',
    'pulse-v2-pause': 'wss://pulse-v2-api.mobula.io',
  },
});
```

MTT uses this to allow users to configure custom endpoints from the UI (stored in localStorage).

***

## Unsubscribing and Cleanup

Always clean up subscriptions when your component unmounts or when you no longer need the data:

```typescript theme={null}
// Store the subscription ID
const subscriptionId = client.streams.subscribe('pulse-v2', payload, callback);

// Later — unsubscribe cleanly
await client.streams.unsubscribe('pulse-v2', subscriptionId);

// Or close the entire WebSocket connection for this stream type
client.streams.close('pulse-v2');
```

In React, handle this in a cleanup function:

```typescript theme={null}
useEffect(() => {
  const id = client.streams.subscribe('pulse-v2', payload, handleMessage);

  return () => {
    client.streams.unsubscribe('pulse-v2', id);
  };
}, [payload]);
```

***

## Full React Example

Here's a minimal but complete React hook for Pulse V2, inspired by MTT's implementation:

```typescript theme={null}
import { useEffect, useRef, useState, useMemo, useCallback } from 'react';
import { MobulaClient } from '@mobula/sdk';

interface PulseToken {
  address?: string;
  chainId?: string;
  name?: string;
  symbol?: string;
  logo?: string;
  marketCap?: number;
  holders_count?: number;
  price_change_24h?: number;
  created_at?: string;
  [key: string]: unknown;
}

type ViewName = 'new' | 'bonding' | 'bonded';

export function usePulseStream(apiKey: string, chainIds: string[] = ['solana:solana']) {
  const clientRef = useRef<MobulaClient | null>(null);
  const subIdRef = useRef<string | null>(null);

  const [tokens, setTokens] = useState<Record<ViewName, PulseToken[]>>({
    new: [], bonding: [], bonded: [],
  });
  const [loading, setLoading] = useState(true);

  const getClient = useCallback(() => {
    if (!clientRef.current) {
      clientRef.current = new MobulaClient({
        restUrl: 'https://pulse-v2-api.mobula.io',
        apiKey,
      });
    }
    return clientRef.current;
  }, [apiKey]);

  const payload = useMemo(() => ({
    assetMode: true,
    compressed: false,
    views: [
      { name: 'new', model: 'new' as const, chainId: chainIds, limit: 50 },
      { name: 'bonding', model: 'bonding' as const, chainId: chainIds, limit: 50 },
      { name: 'bonded', model: 'bonded' as const, chainId: chainIds, limit: 50 },
    ],
  }), [chainIds]);

  useEffect(() => {
    const client = getClient();

    // 1. REST for fast initial load
    client.fetchPulseV2(payload).then((data) => {
      setTokens({
        new: Array.isArray(data.new?.data) ? data.new.data : [],
        bonding: Array.isArray(data.bonding?.data) ? data.bonding.data : [],
        bonded: Array.isArray(data.bonded?.data) ? data.bonded.data : [],
      });
      setLoading(false);
    });

    // 2. WebSocket for live updates
    subIdRef.current = client.streams.subscribe('pulse-v2', payload, (msg: any) => {
      if (msg.type === 'init') {
        setTokens({
          new: Array.isArray(msg.payload.new?.data) ? msg.payload.new.data : [],
          bonding: Array.isArray(msg.payload.bonding?.data) ? msg.payload.bonding.data : [],
          bonded: Array.isArray(msg.payload.bonded?.data) ? msg.payload.bonded.data : [],
        });
        setLoading(false);
      }

      if (msg.type === 'update-token' || msg.type === 'new-token') {
        const { viewName, token } = msg.payload;
        setTokens((prev) => {
          const view = prev[viewName as ViewName] || [];
          const idx = view.findIndex((t) => t.address === token.address);
          const updated = idx >= 0
            ? view.map((t, i) => (i === idx ? { ...t, ...token } : t))
            : [token, ...view].slice(0, 50);
          return { ...prev, [viewName]: updated };
        });
      }
    });

    return () => {
      if (subIdRef.current) {
        client.streams.unsubscribe('pulse-v2', subIdRef.current);
        subIdRef.current = null;
      }
    };
  }, [getClient, payload]);

  return { tokens, loading };
}
```

***

## Next Steps

<CardGroup cols={2}>
  <Card title="Pulse V2 API Reference" icon="book" href="/indexing-stream/stream/websocket/pulse-stream-v2">
    Full API reference with all parameters and response schemas
  </Card>

  <Card title="Filter Details" icon="filter" href="/indexing-stream/stream/websocket/filters-details">
    Complete filter reference with operators and examples
  </Card>

  <Card title="Build Full Pulse UI" icon="desktop" href="/guides/build-axiom-pulse-feature">
    Step-by-step guide to building a complete Pulse UI with Next.js
  </Card>

  <Card title="MTT Source Code" icon="github" href="https://github.com/MobulaFi/MTT">
    Explore the full open-source trading terminal
  </Card>
</CardGroup>
