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

# Migrate from the Launchpad GraphQL Stream to Pulse

> Step-by-step guide to migrate from the Codex-compatible onLaunchpadTokenEvent GraphQL subscription to the native Mobula Pulse V2 stream — endpoint mapping, protocol and network ID translation, field mapping, and message handling.

Mobula's [GraphQL API](/api-reference/graphql/overview) is Codex-compatible: the `onLaunchpadTokenEvent` and `onLaunchpadTokenEventBatch` subscriptions accept the same operations a Codex client already sends, which makes it the fastest first step off Codex. The native **Pulse V2 stream** is the recommended production target: instead of a flat event firehose you subscribe to *views* (new / bonding / bonded, or fully custom), each maintained server-side with sorting, limits, and a Prisma-style filter tree over 100+ token statistics.

This guide maps the launchpad subscription to Pulse V2, concept by concept.

<Tip>
  Already on the GraphQL layer? Both APIs run on the same indexing pipeline, so you can migrate view by view and compare outputs side by side with the same API key.
</Tip>

## Endpoint mapping

|               | Launchpad GraphQL stream                                   | Pulse V2                                                                                                                 |
| ------------- | ---------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------ |
| Real-time     | `wss://graphql.mobula.io/graphql` (`graphql-transport-ws`) | `wss://api.mobula.io` — message `type: "pulse-v2"`                                                                       |
| HTTP snapshot | `filterTokens` query                                       | `GET/POST /api/2/pulse` ([GET](/rest-api-reference/endpoint/pulse-get), [POST](/rest-api-reference/endpoint/pulse-post)) |
| Counts only   | —                                                          | `POST /api/2/pulse/pagination`                                                                                           |
| Auth          | `Authorization: YOUR_API_KEY` header                       | `authorization` field in the subscribe message                                                                           |

<Note>Pulse V2 over WebSocket requires a Growth or Enterprise plan.</Note>

## The mental model shift

The launchpad subscription is **event-based**: every push is one lifecycle event (`Deployed`, `Updated`, `Completed`, `Migrated`, …) for one token, and your client assembles the current state.

Pulse V2 is **view-based**: the server maintains the state — each view is a sorted, filtered, limited set of tokens — and streams membership changes:

| Launchpad stream                         | Pulse V2 equivalent                                                                |                 |
| ---------------------------------------- | ---------------------------------------------------------------------------------- | --------------- |
| `eventType: Deployed` / `Created` push   | `new-token` message on a view filtering `bonded: false` (the default `new` view)   |                 |
| `eventType: Updated` push                | `update-token` message on any view containing the token                            |                 |
| `eventType: Completed` / `Migrated` push | `new-token` message on a view filtering `bonded: true` (the default `bonded` view) |                 |
| `removeToken` payload                    | `remove-token` message (`tokenKey` = \`chainId                                     | tokenAddress\`) |
| `onLaunchpadTokenEventBatch`             | Views batch naturally — the `init` message delivers the full snapshot per view     |                 |
| `updatePeriod` input                     | Server-managed update cadence; use `compressed: true` to cut bandwidth             |                 |

So the three lifecycle stages you modeled with `eventType` filters become three concurrent views on **one** WebSocket connection. The default model creates them for you:

```json theme={null}
{
  "type": "pulse-v2",
  "authorization": "YOUR_API_KEY",
  "payload": {
    "model": "default",
    "assetMode": true,
    "chainId": ["solana:solana"],
    "poolTypes": ["pumpfun"],
    "compressed": false
  }
}
```

This yields the views `new` (not yet bonded, sorted by `created_at`), `bonding` (`bonding_percentage < 100`, sorted by graduation progress), and `bonded` (`bonded: true`, sorted by `bonded_at`).

## Step 1 — Translate network IDs

The GraphQL layer uses Codex-style numeric `networkId`; Pulse uses string chain IDs.

* **EVM chains** map directly: numeric `networkId` `N` → `evm:N` (e.g. `8453` → `evm:8453`). [`getNetworks`](/api-reference/graphql/queries/getNetworks) lists them.
* **Non-EVM chains** use fixed string IDs: `solana:solana`, `sui:sui`, `starknet:starknet`. They are covered by [`getNetworkConfigs`](/api-reference/graphql/queries/getNetworkConfigs).
* Token identity changes from Codex's `address:networkId` to Mobula's `chainId|address` (this is the `tokenKey` format in `remove-token` messages).

## Step 2 — Translate protocols to pool types

`protocol` / `protocols` enum values map to Pulse `poolTypes` / factory ids. The authoritative list lives at `GET https://api.mobula.io/api/1/system-metadata` (`data.factories[].name`); the common ones:

| `LaunchpadTokenProtocol` | Pulse `poolTypes` / factory id |
| ------------------------ | ------------------------------ |
| `Pump`                   | `pumpfun`                      |
| `FourMeme`               | `fourmeme`                     |
| `RaydiumLaunchpad`       | `raydium-launchlab`            |
| `BoopFun`                | `boop`                         |
| `MeteoraDBC`             | `meteora-dbc`                  |
| `ZoraV4`                 | `zora`                         |
| `Virtuals`               | `virtuals`                     |
| `Clanker` / `ClankerV4`  | `clanker`                      |
| `HeavenAMM`              | `heaven`                       |
| `Printr`                 | `printr`                       |
| `NadFun`                 | `nadfun`                       |
| `Flaunch`                | `flaunch-v4`                   |
| `Liquid`                 | `liquidlaunch`                 |

See [LaunchpadTokenProtocol](/api-reference/graphql/types/LaunchpadTokenProtocol) for the full factory-id table.

## Step 3 — Translate the subscription into views

Before — one subscription per protocol and event type:

```graphql theme={null}
subscription {
  onLaunchpadTokenEvent(input: { protocol: Pump, eventType: Deployed }) {
    update {
      address
      networkId
      eventType
      price
      marketCap
      holders
      volume1
      token { symbol name }
    }
    removeToken { address networkId reason }
  }
}
```

After — a custom view (or use the default model above):

```json theme={null}
{
  "type": "pulse-v2",
  "authorization": "YOUR_API_KEY",
  "payload": {
    "assetMode": true,
    "views": [
      {
        "name": "pump-new",
        "chainId": ["solana:solana"],
        "poolTypes": ["pumpfun"],
        "sortBy": "created_at",
        "sortOrder": "desc",
        "limit": 50,
        "filters": {
          "bonded": { "equals": false }
        }
      }
    ]
  }
}
```

Anything you expressed with the subscription input's `filters` / `rankings` carries over to the view's `filters` / `sortBy` — Pulse filters support the full `AND` / `OR` / `NOT` tree with `equals`, `not`, `gte`, `gt`, `lte`, `lt`, `in`, `notIn`, `contains`, `startsWith`, `endsWith`. See the [Filter Details](/indexing-stream/stream/websocket/filters-details) reference.

## Step 4 — Translate fields

<Warning>
  Pulse **filters** use snake\_case (`market_cap`, `holders_count`); the **response** token object uses camelCase (`token.holdersCount`, `token.top10HoldingsPercentage`).
</Warning>

| Launchpad output field                         | Pulse filter field                              | Notes                                                                             |
| ---------------------------------------------- | ----------------------------------------------- | --------------------------------------------------------------------------------- |
| `price`                                        | `latest_price`                                  |                                                                                   |
| `marketCap`                                    | `market_cap`                                    | String in GraphQL, number in Pulse                                                |
| `holders`                                      | `holders_count`                                 |                                                                                   |
| `buyCount1` / `sellCount1`                     | `buys_1h` / `sells_1h`                          | All timeframes available: `1min`, `5min`, `15min`, `1h`, `4h`, `6h`, `12h`, `24h` |
| `volume1`                                      | `volume_1h`                                     | Plus `volume_buy_*` / `volume_sell_*` splits                                      |
| `transactions1`                                | `trades_1h`                                     |                                                                                   |
| `totalFees1`, `poolFees1`, …                   | `fees_paid_1h`                                  |                                                                                   |
| `sniperCount` / `sniperHeldPercentage`         | `snipers_count` / snipers holdings percentage   |                                                                                   |
| `bundlerCount` / `bundlerHeldPercentage`       | `bundlers_count` / bundlers holdings percentage |                                                                                   |
| `insiderCount` / `insiderHeldPercentage`       | `insiders_count` / insiders holdings percentage |                                                                                   |
| `devHeldPercentage`                            | dev holdings percentage                         | Response: `token.devHoldingsPercentage`                                           |
| `top10HoldersPercent`                          | `top_10_holdings_percentage`                    | Response: `token.top10HoldingsPercentage`                                         |
| `launchpadGraduationPercent`                   | `bonding_percentage`                            |                                                                                   |
| `launchpadCompleted` / `launchpadMigrated`     | `bonded`                                        |                                                                                   |
| `launchpadCompletedAt` / `launchpadMigratedAt` | `bonded_at`                                     |                                                                                   |

Pulse additionally exposes fields the launchpad stream never had — organic (bot-excluded) volume/trade series, fresh/pro/smart trader counts, ATH/ATL, security data — see the [Token Data Model](/indexing-stream/stream/websocket/pulse-stream-v2#token-data-model).

## Step 5 — Handle the message loop

Replace your `graphql-transport-ws` `next`-message handler with five message types:

1. **`init`** — full snapshot per view. Replaces your initial `filterTokens` backfill query; render it as-is.
2. **`new-token`** — a token entered a view (deploy in `new`, graduation in `bonded`, …).
3. **`update-token`** — stats changed for a token already in a view.
4. **`remove-token`** — token left a view; delete by `tokenKey` (`chainId|tokenAddress`).
5. **`sync`** — the server resets the view. **You must discard local view state and replace it with the sync payload** — do not merge.

Operational bits:

* **Keepalive**: send `{"event":"ping"}` every 30–60 s.
* **Pause instead of resubscribe**: `{"type": "pulse-pause", "payload": {"action": "pause", "views": ["pump-new"]}}` (and `"unpause"`) — no reconnect, no snapshot replay.
* **Compression**: `compressed: true` enables gzip payloads.

## Step 6 — Validate the migration

* Run both streams side by side for a few days before cutting over; compare **token membership per view**, not raw event counts — an event-based and a view-based stream will never emit the same number of messages for the same reality.
* When counts differ on trades/volume, compare on unique transaction hashes: providers count multi-leg swaps differently (see [benchmarks](/streams-benchmarks)).
* Test with the launchpads you actually track — validate `poolTypes` values against `/api/1/system-metadata` rather than hardcoding.

## Related documentation

<CardGroup>
  <Card title="Pulse Stream V2" icon="bolt" href="/indexing-stream/stream/websocket/pulse-stream-v2">
    Full WebSocket reference — views, filters, message formats.
  </Card>

  <Card title="Pulse WSS SDK Integration" icon="plug" href="/guides/pulse-wss-sdk-integration">
    Integrate with `@mobula/sdk`, with open-source examples from MTT.
  </Card>

  <Card title="onLaunchpadTokenEvent" icon="code-branch" href="/api-reference/graphql/subscriptions/onLaunchpadTokenEvent">
    The GraphQL subscription this guide migrates from.
  </Card>

  <Card title="Build an Axiom-style Pulse" icon="rocket" href="/cookbooks/build-axiom-pulse-feature">
    End-to-end cookbook for a token discovery feed.
  </Card>
</CardGroup>
