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

# Token Filters Stream

> Next-generation WebSocket stream for real-time token and market monitoring with partial updates and real-time view synchronization

<Warning>
  **Alpha feature**: Token Filters access is gated. Ask the Mobula team to enable it for your account. Until then, use Pulse; it works the same way for the same view/filter workflow.
</Warning>

<Tip>
  **Filter Reference**: For complete filter documentation including all available fields and operators, see [Token Filters Reference](/indexing-stream/stream/websocket/token-filters-reference). All available factory names and pool types can be found at: [https://api.mobula.io/api/1/system-metadata](https://api.mobula.io/api/1/system-metadata)
</Tip>

<Tip>
  **Migrating from Pulse V2?** Check out the [Migration Guide](/indexing-stream/stream/websocket/token-filters-migration) for step-by-step instructions.
</Tip>

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

## What's New in Token Filters

Token Filters Stream introduces significant improvements over Pulse V2:

* **Clean Data Models**: Output follows exactly `TokenDetailsOutput` and `MarketDetailsOutput` schemas with consistent camelCase naming
* **Partial Updates**: Only changed fields are sent, reducing bandwidth and JSON parsing overhead
* **Real-time View Updates**: Views are updated in real-time based on incoming swaps - no more periodic sync messages
* **REST API Initialization**: Initialize views via REST API before connecting to WebSocket for better control
* **WebSocket-only Subscriptions**: Send the complete `mode` + `views` payload directly over WebSocket when you do not need a REST init response first
* **OHLCV Attachments**: Add historical candles to initial view results and receive live candle updates per swap
* **Improved Delete Messages**: Delete messages now use structured objects instead of pipe-separated selectors
* **Consistent camelCase**: All input parameters use camelCase matching the output format

## Architecture Overview

### Connection Flow

You can connect to Token Filters in two ways:

1. **WebSocket Only** (recommended): Send the full payload directly via WebSocket
2. **REST + WebSocket**: Initialize via REST API first, then connect to WebSocket

```
┌─────────────────────────────────────────────────────────────────┐
│                    Option 1: WebSocket Only                      │
├─────────────────────────────────────────────────────────────────┤
│   WebSocket Connect  ──▶  Send Full Payload  ──▶  Real-time     │
│   (wss://api.mobula.io)   (views + filters)      Updates        │
└─────────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────────┐
│                    Option 2: REST + WebSocket                    │
├─────────────────────────────────────────────────────────────────┤
│   REST API  ──▶  Get Initial Data  ──▶  WebSocket  ──▶  Updates │
│   (POST)         + subscriptionId       (subscribe)             │
└─────────────────────────────────────────────────────────────────┘
```

<Note>
  **Important**: Even when using WebSocket, you must send the **complete payload** with `mode` and `views` - not just a `subscriptionId`. The REST API is optional and only useful if you need the initial data before connecting.
</Note>

<Note>
  Token Filters WebSocket accepts the same GraphQL/Codex-style aliases as the REST token filters, including `marketCap`, `sellCount4`, `sniperCount`, and `bundlerCount`. See the [Token Filters Reference](/indexing-stream/stream/websocket/token-filters-reference#graphql--codex-compatible-aliases) for the full alias table.
</Note>

## Modes

Token Filters supports two modes:

| Mode     | Description                                  | Output Schema         |
| -------- | -------------------------------------------- | --------------------- |
| `token`  | Token-based analytics and statistics         | `TokenDetailsOutput`  |
| `market` | Pool/market-based data with pair information | `MarketDetailsOutput` |

## Step 1: Initialize Views via REST API

### Endpoint

```
POST /api/2/token/filters
```

### Request Body

```json theme={null}
{
  "mode": "token",
  "views": {
    "new-tokens": {
      "sortBy": "createdAt",
      "sortOrder": "desc",
      "limit": 50,
      "filters": {
        "chainId": { "in": ["solana:solana"] },
        "source": { "in": ["pumpfun"] },
        "bonded": { "equals": false }
      }
    },
    "trending": {
      "sortBy": "trendingScore5min",
      "sortOrder": "desc",
      "limit": 30,
      "filters": {
        "chainId": { "in": ["solana:solana", "evm:8453"] },
        "source": { "in": ["pumpfun", "moonshot-evm"] }
      }
    }
  }
}
```

### Response

```json theme={null}
{
  "subscriptionId": "sub_abc123xyz",
  "views": {
    "new-tokens": {
      "data": [
        {
          "address": "DPQxrrxJfTrLEjyknrUBcMDsAMYbAQJMnKTuJh4kbonk",
          "chainId": "solana:solana",
          "symbol": "EXAMPLE",
          "name": "Example Token",
          "priceUSD": 0.000123,
          "marketCapUSD": 123000,
          "volumeUSD24h": 50000,
          ...
        }
      ]
    },
    "trending": {
      "data": [...]
    }
  }
}
```

## Step 2: Connect to WebSocket

### Connection

```
wss://api.mobula.io
```

### Subscribe Message

Send the **complete payload** with your views configuration:

```json theme={null}
{
  "type": "token-filters",
  "authorization": "YOUR_API_KEY",
  "payload": {
    "mode": "token",
    "delta": true,
    "maxUpdatesPerMinute": 120,
    "views": {
      "new-tokens": {
        "sortBy": "createdAt",
        "sortOrder": "desc",
        "limit": 50,
        "filters": {
          "chainId": { "in": ["solana:solana"] },
          "source": { "in": ["pumpfun"] },
          "bonded": { "equals": false }
        }
      },
      "trending": {
        "sortBy": "trendingScore5min",
        "sortOrder": "desc",
        "limit": 30,
        "filters": {
          "chainId": { "in": ["solana:solana", "evm:8453"] }
        }
      }
    }
  }
}
```

| Payload field         | Type                    | Default     | Description                                                                                       |
| --------------------- | ----------------------- | ----------- | ------------------------------------------------------------------------------------------------- |
| `mode`                | `"token"` \| `"market"` | Required    | Selects token output or market/pool output                                                        |
| `views`               | object                  | Required    | Object keyed by view name. Each value is a view definition                                        |
| `delta`               | boolean                 | `true`      | When true, `update-token` / `update-market` messages contain only changed fields plus identifiers |
| `maxUpdatesPerMinute` | number                  | No throttle | Per-entity update throttle for the subscription                                                   |

<Warning>
  **Do NOT send just a `subscriptionId`**. You must always send the full `mode` and `views` configuration, even if you previously called the REST API. The WebSocket subscription is stateless.
</Warning>

## Preset Models

Use preset models for common use cases without defining full custom views:

```json theme={null}
{
  "mode": "token",
  "views": {
    "new-tokens": {
      "model": "new",
      "filters": {
        "chainId": { "in": ["solana:solana"] },
        "source": { "in": ["pumpfun"] }
      }
    }
  }
}
```

### Available Presets

| Model        | Description                    | Sort By                        | Default Filters                                                                                                         |
| ------------ | ------------------------------ | ------------------------------ | ----------------------------------------------------------------------------------------------------------------------- |
| `new`        | Newly created launchpad tokens | `createdAt` desc               | `bonded: false`, `source: { in: launchpad pools }`                                                                      |
| `bonding`    | Tokens in bonding phase        | `bondingPercentage` desc       | `bondingPercentage: { gt: 0, lt: 100 }`, `volume1hUSD: { gte: 10 }`, `bonded: false`, `bondedAt: null`, launchpad pools |
| `bonded`     | Recently bonded tokens         | `bondedAt` desc                | `bonded: true`, `bondedAt: { not: null }`, launchpad pools                                                              |
| `trending`   | Fee-weighted trending tokens   | `feesPaid15minUSD` desc        | Liquidity, fee, volume, holder-concentration, transfer-tax and chain-specific thresholds                                |
| `explorer`   | Explorer view by trending      | `trendingScore1h` desc         | -                                                                                                                       |
| `topGainers` | Top price gainers              | `priceChange6hPercentage` desc | `volume1hUSD: { gte: 500 }`                                                                                             |
| `surge`      | Small-cap surge tokens         | `surgeScore` desc              | `liquidityUSD: { gte: 3000, lte: 50000 }`, `surgeScore: { gt: 0 }`                                                      |

### OHLCV Mode

Enable OHLCV candlestick data for each token in a view by setting `ohlcv: true` in the view definition.

**Parameters:**

| Parameter        | Type      | Default | Description                                                                     |
| ---------------- | --------- | ------- | ------------------------------------------------------------------------------- |
| `ohlcv`          | `boolean` | `false` | Enable OHLCV candle data                                                        |
| `ohlcvTimeframe` | `string`  | `"5m"`  | Candle timeframe: `1s`, `5s`, `15s`, `1m`, `5m`, `15m`, `30m`, `1h`, `4h`, `1d` |

**Constraints:**

* When `ohlcv: true`, the max `limit` is **15 tokens** (automatically clamped)
* Up to **200 candles** per token are returned on init when history is available
* Candle format: `{ o, h, l, c, v, t }` (open, high, low, close, volume, timestamp ms)

**Example (surge + OHLCV):**

```json theme={null}
{
  "mode": "token",
  "views": {
    "surge-ohlcv": {
      "model": "surge",
      "ohlcv": true,
      "ohlcvTimeframe": "5m",
      "limit": 10
    }
  }
}
```

**Init response** includes `ohlcv` array per token:

```json theme={null}
{
  "subscriptionId": "sub_abc123",
  "views": {
    "surge-ohlcv": {
      "data": [
        {
          "address": "TokenAddress...",
          "chainId": "solana:solana",
          "priceUSD": 0.001,
          "ohlcv": [
            { "o": 0.0009, "h": 0.0012, "l": 0.0008, "c": 0.001, "v": 5000, "t": 1709300000000 },
            { "o": 0.001, "h": 0.0013, "l": 0.0009, "c": 0.0012, "v": 3200, "t": 1709300300000 }
          ]
        }
      ]
    }
  }
}
```

**Real-time candle updates** are sent via `update-token-ohlcv` messages when new swaps occur:

```json theme={null}
{
  "type": "update-token-ohlcv",
  "payload": {
    "viewName": "surge-ohlcv",
    "token": { "address": "TokenAddress...", "chainId": "solana:solana" },
    "candle": { "o": 0.001, "h": 0.0015, "l": 0.001, "c": 0.0014, "v": 8500, "t": 1709300600000 }
  }
}
```

<Note>
  When a new candle period starts, the previous candle's `close` is used as the new candle's `open`. Volume resets to 0 for each new period.
</Note>

## Message Types

### Update Message (Partial)

When token/market data changes, only the modified fields are sent:

```json theme={null}
{
  "type": "update-token",
  "payload": {
    "viewName": "trending",
    "token": {
      "address": "DPQxrrxJfTrLEjyknrUBcMDsAMYbAQJMnKTuJh4kbonk",
      "chainId": "solana:solana",
      "priceUSD": 0.000135,
      "priceChange1hPercentage": 15.2,
      "volume1hUSD": 55000
    }
  }
}
```

<Note>
  **Partial Updates**: Only changed fields are included. Apply these as patches to your local state. The `address` and `chainId` are always included for identification.
</Note>

### New Token/Market Message

When a new token enters a view:

```json theme={null}
{
  "type": "new-token",
  "payload": {
    "viewName": "new-tokens",
    "token": {
      "address": "NewTokenAddress123",
      "chainId": "solana:solana",
      "symbol": "NEW",
      "name": "New Token",
      "decimals": 6,
      "priceUSD": 0.0001,
      "priceToken": 0.00000005,
      "priceTokenString": "0.00000005",
      "marketCapUSD": 100000,
      "volume1hUSD": 5000,
      "volume24hUSD": 25000,
      "trades1h": 150,
      "trades24h": 800,
      "holdersCount": 250,
      "liquidityUSD": 15000,
      "bonded": false,
      "bondingPercentage": 45.5,
      "createdAt": "2025-01-23T10:30:00.000Z",
      "socials": {
        "twitter": "https://twitter.com/newtoken",
        "website": "https://newtoken.com",
        "telegram": null,
        "others": null
      },
      ...
    }
  }
}
```

### Remove Token/Market Message

When a token exits a view:

```json theme={null}
{
  "type": "remove-token",
  "payload": {
    "viewName": "trending",
    "token": {
      "chainId": "solana:solana",
      "address": "DPQxrrxJfTrLEjyknrUBcMDsAMYbAQJMnKTuJh4kbonk"
    }
  }
}
```

<Note>
  **Improved Format**: Token Filters uses a structured object `{ chainId, address }` instead of the V2 pipe-separated format `chainId|address`.
</Note>

### OHLCV Update Message

When OHLCV mode is enabled, candle updates are sent on each swap:

```json theme={null}
{
  "type": "update-token-ohlcv",
  "payload": {
    "viewName": "surge-view",
    "token": {
      "chainId": "solana:solana",
      "address": "DPQxrrxJfTrLEjyknrUBcMDsAMYbAQJMnKTuJh4kbonk"
    },
    "candle": {
      "o": 0.001,
      "h": 0.0013,
      "l": 0.0009,
      "c": 0.0012,
      "v": 6500,
      "t": 1709300000000
    }
  }
}
```

## OHLCV Mode

Enable OHLCV mode to receive historical candlestick data on init and real-time candle updates on each swap.

### View Definition with OHLCV

Add `ohlcv: true` and optionally `ohlcvTimeframe` to a view definition:

```json theme={null}
{
  "mode": "token",
  "views": {
    "surge-candles": {
      "model": "surge",
      "ohlcv": true,
      "ohlcvTimeframe": "5m"
    }
  }
}
```

### Parameters

| Parameter        | Type    | Default | Description                                                                              |
| ---------------- | ------- | ------- | ---------------------------------------------------------------------------------------- |
| `ohlcv`          | boolean | `false` | Enable OHLCV candlestick data                                                            |
| `ohlcvTimeframe` | string  | `"5m"`  | Candle timeframe. Allowed: `1s`, `5s`, `15s`, `1m`, `5m`, `15m`, `30m`, `1h`, `4h`, `1d` |

### Constraints

* When `ohlcv: true`, the view **limit is clamped to 15** (max 15 tokens per view)
* Each token receives up to **200 historical candles** on init when history is available
* Real-time candle updates are sent via `update-token-ohlcv` messages

### Init Response with OHLCV

When OHLCV is enabled, each token in the response includes an `ohlcv` array:

```json theme={null}
{
  "subscriptionId": "sub_abc123",
  "views": {
    "surge-candles": {
      "data": [
        {
          "address": "DPQxrrxJfTrLEjyknrUBcMDsAMYbAQJMnKTuJh4kbonk",
          "chainId": "solana:solana",
          "priceUSD": 0.001,
          "ohlcv": [
            { "o": 0.001, "h": 0.0012, "l": 0.0009, "c": 0.0011, "v": 5000, "t": 1709300000000 },
            { "o": 0.0011, "h": 0.0015, "l": 0.001, "c": 0.0013, "v": 3200, "t": 1709300300000 }
          ]
        }
      ]
    }
  }
}
```

### Candle Fields

| Field | Description                                 |
| ----- | ------------------------------------------- |
| `o`   | Open price                                  |
| `h`   | High price                                  |
| `l`   | Low price                                   |
| `c`   | Close price                                 |
| `v`   | Volume (USD)                                |
| `t`   | Candle timestamp (ms, aligned to timeframe) |

## Token Data Model (mode: "token")

The token mode returns data following the `TokenDetailsOutput` schema:

```typescript theme={null}
interface TokenDetailsOutput {
  // Identification
  address: string;
  chainId: string;
  symbol: string | null;
  name: string | null;
  decimals: number;
  id: number | null;

  // Pricing
  priceUSD: number;
  priceToken: number;
  priceTokenString: string;

  // Market Data
  marketCapUSD: number;
  marketCapDilutedUSD: number;
  totalSupply: number;
  circulatingSupply: number;
  approximateReserveUSD: number;
  approximateReserveToken: number;
  approximateReserveTokenRaw: string;

  // Liquidity
  liquidityUSD: number;
  liquidityMaxUSD: number;

  // Bonding
  bonded: boolean;
  bondingPercentage: number;
  bondingCurveAddress: string | null;
  preBondingFactory: string;
  bondedAt: Date | null;

  // Price Changes
  priceChange1minPercentage: number;
  priceChange5minPercentage: number;
  priceChange1hPercentage: number;
  priceChange4hPercentage: number;
  priceChange6hPercentage: number;
  priceChange12hPercentage: number;
  priceChange24hPercentage: number;

  // Volume (USD)
  volume1minUSD: number;
  volume5minUSD: number;
  volume15minUSD: number;
  volume1hUSD: number;
  volume4hUSD: number;
  volume6hUSD: number;
  volume12hUSD: number;
  volume24hUSD: number;

  // Volume Buy/Sell
  volumeBuy1minUSD: number;
  volumeBuy1hUSD: number;
  volumeBuy24hUSD: number;
  volumeSell1minUSD: number;
  volumeSell1hUSD: number;
  volumeSell24hUSD: number;
  // ... all timeframes

  // Trades
  trades1min: number;
  trades5min: number;
  trades1h: number;
  trades24h: number;
  // ... all timeframes

  // Buys/Sells
  buys1min: number;
  buys1h: number;
  buys24h: number;
  sells1min: number;
  sells1h: number;
  sells24h: number;
  // ... all timeframes

  // Unique Participants
  buyers1h: number;
  buyers24h: number;
  sellers1h: number;
  sellers24h: number;
  traders1h: number;
  traders24h: number;
  // ... all timeframes

  // Organic Metrics (excluding bot activity)
  organicTrades1h: number;
  organicVolume1hUSD: number;
  organicBuyers1h: number;
  // ... all organic fields

  // Trending Scores
  trendingScore1min: number;
  trendingScore5min: number;
  trendingScore15min: number;
  trendingScore1h: number;
  trendingScore4h: number;
  trendingScore6h: number;
  trendingScore12h: number;
  trendingScore24h: number;

  // Holdings Analysis
  holdersCount: number;
  top10HoldingsPercentage: number;
  top50HoldingsPercentage: number;
  top100HoldingsPercentage: number;
  top200HoldingsPercentage: number;
  devHoldingsPercentage: number;
  insidersHoldingsPercentage: number;
  bundlersHoldingsPercentage: number;
  snipersHoldingsPercentage: number;
  proTradersHoldingsPercentage: number;
  freshTradersHoldingsPercentage: number;
  smartTradersHoldingsPercentage: number;

  // Trader Categories
  insidersCount: number;
  bundlersCount: number;
  snipersCount: number;
  freshTradersCount: number;
  proTradersCount: number;
  smartTradersCount: number;
  smartTradersBuys: number;
  freshTradersBuys: number;
  proTradersBuys: number;

  // Fees
  feesPaid1hUSD: number;
  feesPaid24hUSD: number;
  totalFeesPaidUSD: number;
  totalFeesPaidNativeRaw: string;
  // ... all timeframes

  // ATH/ATL
  athUSD: number;
  atlUSD: number;
  athDate: Date;
  atlDate: Date;

  // Metadata
  logo: string | null;
  originLogoUrl: string | null;
  description: string | null;
  deployer: string | null;
  deployerMigrationsCount: number;
  deployerTokensCount: number;
  rank: number | null;
  cexs: string[];

  // Social
  socials: {
    twitter: string | null;
    website: string | null;
    telegram: string | null;
    others: Record<string, unknown> | undefined;
    uri: string | undefined;
  };

  // Security
  security: SecurityFlags | null;

  // Twitter Analysis
  twitterReusesCount: number;
  twitterRenameCount: number;
  twitterRenameHistory: Array<{
    username: string;
    lastChecked: string;
  }>;

  // DexScreener
  dexscreenerListed: boolean;
  dexscreenerHeader: string | null;
  dexscreenerAdPaid: boolean;
  dexscreenerAdPaidDate: Date | null;
  dexscreenerSocialPaid: boolean;
  dexscreenerSocialPaidDate: Date | null;
  dexscreenerBoosted: boolean;
  dexscreenerBoostedDate: Date | null;
  dexscreenerBoostedAmount: number;

  // Livestream
  liveStatus: string | null;
  liveThumbnail: string | null;
  livestreamTitle: string | null;
  liveReplyCount: number | null;

  // Exchange & Factory
  exchange: { name: string; logo: string } | undefined;
  factory: string | null;
  source: string | null;
  sourceFactory: string | null;
  poolAddress: string;
  blockchain: string;
  tokenType: string;

  // Timestamps
  createdAt: Date | null;
  latestTradeDate: Date | null;

  // Internationalization
  i18n: Record<string, unknown> | null;
}
```

## Market Data Model (mode: "market")

The market mode returns data following the `MarketDetailsOutput` schema with pair information:

```typescript theme={null}
interface MarketDetailsOutput {
  // Pool Information
  address: string;
  blockchain: string;
  type: string;
  factory: string | null;
  createdAt: Date | null;
  latestTradeDate: Date | null;

  // Pair Tokens
  base: TokenData;
  quote: TokenData;
  baseToken: "token0" | "token1";
  quoteToken: "token0" | "token1";

  // Pricing
  priceUSD: number;
  priceToken: number;
  priceTokenString: string;
  liquidityUSD: number;

  // Bonding
  bonded: boolean;
  bondingPercentage: number;
  preBondingPoolAddress: string | null;
  sourceFactory: string | null;

  // Price Changes
  priceChange1minPercentage: number;
  priceChange5minPercentage: number;
  priceChange1hPercentage: number;
  // ... all timeframes

  // Volume, Trades, Participants
  // Same structure as TokenDetailsOutput

  // Holdings, Security, Socials
  // Inherited from base token

  // Exchange
  exchange: { name: string; logo: string };
}
```

## View Configuration

### View Parameters

| Parameter        | Type            | Required | Description                                                                             |
| ---------------- | --------------- | -------- | --------------------------------------------------------------------------------------- |
| view key         | string          | Yes      | Unique identifier for the view, e.g. `"new-tokens"` in `views`                          |
| `model`          | string          | No       | Preset model: `new`, `bonding`, `bonded`, `trending`, `explorer`, `topGainers`, `surge` |
| `sortBy`         | string          | No       | Field to sort by (camelCase)                                                            |
| `sortOrder`      | "asc" \| "desc" | No       | Sort direction                                                                          |
| `limit`          | number          | No       | Max results (default: 50, max: 100)                                                     |
| `offset`         | number          | No       | Pagination offset                                                                       |
| `filters`        | object          | No       | Filter configuration                                                                    |
| `selectors`      | array           | No       | Explicit token or market selectors: `{ "chainId": "...", "address": "..." }`            |
| `ohlcv`          | boolean         | No       | Include OHLCV history and live candle updates for token views                           |
| `ohlcvTimeframe` | string          | No       | OHLCV timeframe when `ohlcv` is true                                                    |

<Note>
  Use `filters.chainId` for chains and `filters.source` for token launchpads/factories. In `market` mode, use `filters.pool.type` or `filters.type` for pool type filtering.
</Note>

### Filter Operators

| Operator     | Description           | Example                                      |
| ------------ | --------------------- | -------------------------------------------- |
| `equals`     | Exact match           | `{ "bonded": { "equals": true } }`           |
| `gte`        | Greater than or equal | `{ "marketCapUSD": { "gte": 10000 } }`       |
| `lte`        | Less than or equal    | `{ "volume1hUSD": { "lte": 100000 } }`       |
| `gt`         | Greater than          | `{ "holdersCount": { "gt": 100 } }`          |
| `lt`         | Less than             | `{ "bondingPercentage": { "lt": 100 } }`     |
| `not`        | Not equal             | `{ "source": { "not": null } }`              |
| `in`         | In array              | `{ "chainId": { "in": ["solana:solana"] } }` |
| `contains`   | String contains       | `{ "symbol": { "contains": "DOGE" } }`       |
| `startsWith` | String prefix         | `{ "chainId": { "startsWith": "ton" } }`     |
| `endsWith`   | String suffix         | `{ "symbol": { "endsWith": "INU" } }`        |

## Custom Views Example

```json theme={null}
{
  "mode": "token",
  "views": {
    "high-volume-pumpfun": {
      "sortBy": "volume1hUSD",
      "sortOrder": "desc",
      "limit": 50,
      "filters": {
        "chainId": { "in": ["solana:solana"] },
        "source": { "in": ["pumpfun"] },
        "volume1hUSD": { "gte": 5000 },
        "marketCapUSD": { "gte": 10000 },
        "bonded": { "equals": false }
      }
    },
    "whale-accumulation": {
      "sortBy": "volumeBuy1hUSD",
      "sortOrder": "desc",
      "limit": 30,
      "filters": {
        "chainId": { "in": ["solana:solana", "evm:8453"] },
        "volumeBuy1hUSD": { "gte": 10000 },
        "top10HoldingsPercentage": { "lte": 30 },
        "holdersCount": { "gte": 100 }
      }
    },
    "safe-tokens": {
      "sortBy": "trendingScore1h",
      "sortOrder": "desc",
      "limit": 25,
      "filters": {
        "chainId": { "in": ["solana:solana"] },
        "security": {
          "noMintAuthority": { "equals": true },
          "isBlacklisted": { "equals": false }
        },
        "devHoldingsPercentage": { "lte": 5 }
      }
    }
  }
}
```

## View Management

### Pause Views

Temporarily stop receiving updates for specific views:

```json theme={null}
{
  "type": "token-filters-pause",
  "payload": {
    "action": "pause",
    "views": ["high-volume-pumpfun", "whale-accumulation"]
  }
}
```

### Unpause Views

Resume updates:

```json theme={null}
{
  "type": "token-filters-pause",
  "payload": {
    "action": "unpause",
    "views": ["high-volume-pumpfun"]
  }
}
```

## Unsubscribe

### Unsubscribe from All

```json theme={null}
{
  "type": "unsubscribe",
  "authorization": "YOUR_API_KEY",
  "payload": {
    "type": "token-filters"
  }
}
```

### Unsubscribe from Specific View

```json theme={null}
{
  "type": "unsubscribe",
  "authorization": "YOUR_API_KEY",
  "payload": {
    "type": "token-filters",
    "viewName": "high-volume-pumpfun"
  }
}
```

### Unsubscribe by Subscription ID

```json theme={null}
{
  "type": "unsubscribe",
  "authorization": "YOUR_API_KEY",
  "payload": {
    "type": "token-filters",
    "subscriptionId": "sub_abc123xyz"
  }
}
```

## Connection Keepalive

Send periodic ping messages to maintain the connection:

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

The server responds with a pong message.

<Tip>Send ping messages every 30-60 seconds for long-lived connections.</Tip>

## Complete TypeScript Example

```typescript theme={null}
import WebSocket from 'ws';

interface TokenFiltersConfig {
  apiKey: string;
  mode: 'token' | 'market';
  views: Record<string, ViewConfig>;
  delta?: boolean;
  maxUpdatesPerMinute?: number;
}

interface ViewConfig {
  model?: 'new' | 'bonding' | 'bonded' | 'trending' | 'explorer' | 'topGainers' | 'surge';
  sortBy?: string;
  sortOrder?: 'asc' | 'desc';
  limit?: number;
  offset?: number;
  filters?: Record<string, unknown>;
  ohlcv?: boolean;
  ohlcvTimeframe?: string;
}

class TokenFiltersClient {
  private ws: WebSocket | null = null;
  private state: Map<string, Map<string, unknown>> = new Map();

  constructor(private config: TokenFiltersConfig) {}

  async initialize(): Promise<void> {
    // Initialize view state maps
    for (const viewName of Object.keys(this.config.views)) {
      this.state.set(viewName, new Map());
    }

    // Connect to WebSocket directly
    await this.connect();
  }

  private async connect(): Promise<void> {
    this.ws = new WebSocket('wss://api.mobula.io');

    this.ws.on('open', () => {
      // Send FULL payload with mode and views (not just subscriptionId)
      this.ws!.send(JSON.stringify({
        type: 'token-filters',
        authorization: this.config.apiKey,
        payload: {
          mode: this.config.mode,
          delta: this.config.delta ?? true,
          maxUpdatesPerMinute: this.config.maxUpdatesPerMinute,
          views: this.config.views
        }
      }));
    });

    this.ws.on('message', (data) => {
      const message = JSON.parse(data.toString());
      this.handleMessage(message);
    });

    // Keepalive
    setInterval(() => {
      if (this.ws?.readyState === WebSocket.OPEN) {
        this.ws.send(JSON.stringify({ event: 'ping' }));
      }
    }, 30000);
  }

  private handleMessage(message: {
    type: string;
    payload: {
      viewName: string;
      token: { address: string; chainId: string; [key: string]: unknown };
    };
  }): void {
    const { type, payload } = message;

    switch (type) {
      case 'new-token':
      case 'new-market': {
        const viewState = this.state.get(payload.viewName);
        if (viewState) {
          const key = `${payload.token.chainId}|${payload.token.address}`;
          viewState.set(key, payload.token);
          console.log(`[NEW] ${payload.viewName}: ${payload.token.symbol}`);
        }
        break;
      }

      case 'update-token':
      case 'update-market': {
        const viewState = this.state.get(payload.viewName);
        if (viewState) {
          const key = `${payload.token.chainId}|${payload.token.address}`;
          const existing = viewState.get(key) as Record<string, unknown> | undefined;
          if (existing) {
            // Apply partial update
            const updated = { ...existing, ...payload.token };
            viewState.set(key, updated);
            console.log(`[UPDATE] ${payload.viewName}: ${(existing as { symbol?: string }).symbol}`);
          }
        }
        break;
      }

      case 'remove-token':
      case 'remove-market': {
        const viewState = this.state.get(payload.viewName);
        if (viewState) {
          const key = `${payload.token.chainId}|${payload.token.address}`;
          viewState.delete(key);
          console.log(`[REMOVE] ${payload.viewName}: ${payload.token.address}`);
        }
        break;
      }
    }
  }

  getViewData(viewName: string): unknown[] {
    const viewState = this.state.get(viewName);
    return viewState ? Array.from(viewState.values()) : [];
  }
}

// Usage
const client = new TokenFiltersClient({
  apiKey: 'YOUR_API_KEY',
  mode: 'token',
  views: {
    trending: {
      sortBy: 'feesPaid15minUSD',
      sortOrder: 'desc',
      limit: 50,
      filters: {
        chainId: { in: ['solana:solana'] },
        source: { in: ['pumpfun'] }
      }
    }
  }
});

await client.initialize();
```

## Performance Considerations

### View Limits

* Maximum 10 views per connection
* Maximum 100 items per view
* Maximum 1000 total items across all views

### Bandwidth Optimization

* Partial updates significantly reduce bandwidth
* Use `pause` for inactive views
* Filter aggressively to reduce data volume
* Use appropriate `limit` values

### Best Practices

1. Initialize via REST API to get initial state
2. Apply partial updates to local state
3. Use ping/pong for connection health
4. Implement reconnection logic
5. Handle all message types gracefully

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