Solana Tracker LogoSolana Tracker
Swap
Developers
⌘K
Affiliate
All Resources
Pump.fun Graduation API: Curve Alerts and Migrations
Pump.funTypeScriptMay 29, 202612 min readUpdated October 7, 2026Solana Tracker

Pump.fun Graduation API: Curve Alerts and Migrations

Use the Pump.fun graduation API to alert on bonding-curve thresholds, confirm migrations in the graduated room, and reconcile gaps over REST in TypeScript.

  • pumpfun,
  • graduation,
  • bonding-curve,
  • datastream,
  • typescript
Pump.fun API›Solana Data API›Raptor Swap API›

The Pump.fun graduation API in Solana Tracker is a set of Datastream rooms plus one REST endpoint: pumpfun:curve:{percentage} tells you when a token's bonding curve crosses a threshold, graduated tells you when a token has migrated to its DEX pool, and GET /tokens/multi/graduated lists recent graduations so you can fill gaps. Watch the curve room to build a shortlist, confirm on the graduated room, and reconcile over REST after every reconnect.

This guide explains the lifecycle stages, which room or endpoint answers which question, the payloads, a complete TypeScript monitor, and the edge cases that produce false or missed graduation alerts. The data is part of the Pump.fun API, a view of the Solana Data API; keys and plans are in the Data API dashboard.

How Pump.fun graduation works

A Pump.fun token launches on a bonding curve. Buyers push the curve toward completion and the pool's curvePercentage rises; sells push it back down. When the curve completes, liquidity migrates to an AMM pool and the token starts trading there. In Solana Tracker data that shows up as a new pool on the token, typically with market set to "pumpfun-amm" (PumpSwap), alongside or after the original "pumpfun" curve pool.

Three questions follow from that, and each has its own feed:

  1. Which tokens are getting close? Curve threshold alerts and the graduating list.
  2. Which tokens just migrated? The graduated room.
  3. What did I miss while offline, and when did it graduate? REST.

Keep "approaching" and "graduated" as separate states in your system. A token at 90% can stall or reverse, and a curve alert is a signal, not a forecast.

Pump.fun graduation API: rooms and endpoints

SourceAnswersScopeNotes
pumpfun:curve:{pct} roomToken crossed pct% of its curvePump.fun onlySDK: subscribe.curvePercentage("pumpfun", pct), pct 0-100
graduating roomTokens approaching completionAll launchpadsSDK: subscribe.graduating(); docs also list graduating:pumpfun:{sol}
graduated roomTokens that just migratedAll launchpadsSDK: subscribe.graduated(); filter for Pump.fun yourself
GET /tokens/multi/graduatingSnapshot of tokens on the curveFilter by curve, holders, marketminCurve default 40, minHolders default 20
GET /tokens/multi/graduatedRecently graduated tokensAll launchpadslimit up to 500, page, reduceSpam, markets, liquidity and market cap ranges
GET /search with status=graduatedGraduated tokens with graduatedAt (ms)Filter by launchpad=pumpfunUse minGraduatedAt / maxGraduatedAt for time windows

Curve rooms exist for pumpfun, launchpad, boop and meteora-curve. The docs describe graduating:pumpfun:{sol} as Pump.fun tokens with at least that much SOL in the curve; SDK 0.5.0 has no helper for that room (its graduating(n) builds graduating:sol:{n}), so if you need it, join it over a raw WebSocket. The example below uses the curve room, which is Pump.fun-scoped and supported by the SDK.

Payloads

Curve and graduated messages carry the same token / pools / events / risk structure as the rest of Datastream. A trimmed graduated message, based on the documented example:

{
  "type": "message",
  "room": "graduated",
  "data": {
    "token": {
      "symbol": "GRAD",
      "mint": "AmJaZvdNptvofC4qe3tvuBNgqLm65p1of5pk6JFHpump",
      "createdOn": "https://pump.fun",
      "creation": { "creator": "4Rz5xqikxtZ2s7wE9uQ6n2oLXQi6K65XGoYpKxf24Hqo", "created_time": 1723727770 }
    },
    "pools": [
      {
        "poolId": "GmJaZvdNptvofC4qe3tvuBNgqLm65p1of5pk6JFHpump",
        "market": "raydium",
        "liquidity": { "quote": 62, "usd": 8907.76 },
        "marketCap": { "quote": 29.854, "usd": 4289.23 },
        "createdAt": 1723727770351,
        "creation": {
          "creator": "CQdrEsYAxRqkwmpycuTwnMKggr3cr9fqY8Qma4J9TudY",
          "created_tx": "5WYyUdhqy6M83g49rK8goD8HeaVfQpzeKqgXk3Guqh9rFHXWg6jLLaZ4QsCLowMzveec9UtR9bQBdf6iVkQuE6c6",
          "created_time": 1772316351
        }
      }
    ],
    "risk": { "rugged": false, "score": 5 }
  }
}

Read it with three things in mind. The destination market depends on the launchpad and era ("raydium" in the documented example, "pumpfun-amm" for current Pump.fun migrations), so do not hard-code one value. pool.creation describes the creation of that pool (creator, transaction, Unix seconds), which is the closest thing to a migration timestamp in the stream payload. And createdAt / lastUpdated are milliseconds while created_time is seconds.

The curve room payload uses the same pool fields; the useful ones are curvePercentage on the "pumpfun" pool, plus marketCap and liquidity to judge momentum.

Filter the graduated room for Pump.fun

The graduated room covers every launchpad (Pump.fun, letsbonk.fun, Jupiter Studio and others), so the monitor needs a rule for "this one came from Pump.fun". In order of strength:

  1. The mint is on your watchlist because it crossed the Pump.fun curve threshold.
  2. The message still includes the "pumpfun" curve pool.
  3. token.createdOn contains pump.fun.

The companion project adds a fourth, weaker signal (a "pumpfun-amm" pool), because PumpSwap pools can also exist for tokens that never used the curve. Log which rule matched; it makes false positives easy to audit.

Detect Pump.fun graduations in TypeScript

The monitor below joins pumpfun:curve:{CURVE_THRESHOLD} and graduated, keeps a watchlist of tokens that crossed the threshold, reports graduations with the destination pool and how long after the curve alert they migrated, and reconciles with GET /tokens/multi/graduated on every connected event.

npm init -y && npm pkg set type=module
npm install @solana-tracker/data-api@^0.5.0
npm install --save-dev tsx typescript @types/node
node --env-file=.env --import tsx index.ts

.env.example:

# Data API key from /account/data-api (used for the REST gap fill)
ST_API_KEY=
# Datastream key or the full wss://datastream.solanatracker.io/<key> URL (Premium plan or higher)
ST_DATASTREAM_KEY=
# Curve percentage that puts a token on the watchlist, integer 1-100
CURVE_THRESHOLD=90
# Rows of /tokens/multi/graduated to check per (re)connect, 0-500 (0 disables)
BACKFILL_LIMIT=50

index.ts:

import { Client, DataApiError, Datastream, RateLimitError, type PoolInfo, type TokenInfo } from "@solana-tracker/data-api";

const apiKey = process.env.ST_API_KEY?.trim();
const streamKey = process.env.ST_DATASTREAM_KEY?.trim();
if (!apiKey) exit("Missing ST_API_KEY (Data API key from /account/data-api)");
if (!streamKey) exit("Missing ST_DATASTREAM_KEY (Datastream key or wss:// URL)");
const wsUrl = streamKey.startsWith("wss://") ? streamKey : `wss://datastream.solanatracker.io/${streamKey}`;
const CURVE_THRESHOLD = Number(process.env.CURVE_THRESHOLD ?? "90");
const BACKFILL_LIMIT = Number(process.env.BACKFILL_LIMIT ?? "50");
if (!Number.isInteger(CURVE_THRESHOLD) || CURVE_THRESHOLD < 1 || CURVE_THRESHOLD > 100) {
  exit("CURVE_THRESHOLD must be an integer from 1 to 100");
}
if (!Number.isInteger(BACKFILL_LIMIT) || BACKFILL_LIMIT < 0 || BACKFILL_LIMIT > 500) {
  exit("BACKFILL_LIMIT must be an integer from 0 to 500");
}

function exit(message: string): never {
  console.error(message);
  process.exit(1);
}

type TokenLike = { token: TokenInfo; pools: PoolInfo[] };

const client = new Client({ apiKey });
const ds = new Datastream({ wsUrl, autoReconnect: true, reconnectDelay: 1_000, reconnectDelayMax: 30_000, randomizationFactor: 0.5 });

const watchlist = new Map<string, number>(); // mint -> when it crossed the threshold (ms)
const graduated = new Set<string>(); // mints already reported

// The graduated room spans every launchpad: keep only Pump.fun tokens.
function isPumpfun(item: TokenLike): boolean {
  return (
    watchlist.has(item.token.mint) ||
    item.pools?.some((p) => p.market === "pumpfun") ||
    (item.token.createdOn ?? "").includes("pump.fun")
  );
}

// The post-migration pool: never the curve pool, deepest liquidity first.
function destination(item: TokenLike): PoolInfo | undefined {
  return [...(item.pools ?? [])]
    .filter((p) => p.market !== "pumpfun")
    .sort((a, b) => (b.liquidity?.usd ?? 0) - (a.liquidity?.usd ?? 0))[0];
}

function onCurve(item: TokenLike): void {
  const mint = item.token?.mint;
  if (!mint || watchlist.has(mint) || graduated.has(mint)) return;
  watchlist.set(mint, Date.now());
  const curve = item.pools?.find((p) => p.market === "pumpfun")?.curvePercentage;
  console.log(`CURVE>=${CURVE_THRESHOLD}  ${item.token.symbol}  ${mint}  curve ${curve?.toFixed(1) ?? "?"}%`);
}

function onGraduated(item: TokenLike, source: "live" | "rest"): void {
  const mint = item.token?.mint;
  if (!mint || graduated.has(mint) || !isPumpfun(item)) return;
  graduated.add(mint);
  const watchedAt = watchlist.get(mint);
  watchlist.delete(mint);
  const pool = destination(item);
  const waited = watchedAt ? `${Math.round((Date.now() - watchedAt) / 1000)}s after curve alert` : "not on watchlist";
  console.log(
    `GRADUATED (${source})  ${item.token.symbol}  ${mint}  -> ${pool?.market ?? "?"} ${pool?.poolId ?? "?"}  liq $${(pool?.liquidity?.usd ?? 0).toFixed(0)}  ${waited}`,
  );
}

async function withRetry<T>(label: string, fn: () => Promise<T>, attempts = 3): Promise<T> {
  for (let attempt = 1; ; attempt++) {
    let timer: NodeJS.Timeout | undefined;
    try {
      const timeout = new Promise<never>((_, reject) => {
        timer = setTimeout(() => reject(new Error(`${label} timed out`)), 15_000);
      });
      return await Promise.race([fn(), timeout]);
    } catch (error) {
      const retryable =
        error instanceof RateLimitError || !(error instanceof DataApiError) || error.status === undefined || error.status >= 500;
      if (!retryable || attempt >= attempts) throw error;
      const retryAfterMs = error instanceof RateLimitError && error.retryAfter ? error.retryAfter * 1000 : 0;
      await new Promise((r) => setTimeout(r, Math.max(500 * 2 ** (attempt - 1), retryAfterMs)));
    } finally {
      clearTimeout(timer);
    }
  }
}

// Gap fill after every (re)connect, at most once per 15 s.
let lastGapFill = 0;
async function gapFill(): Promise<void> {
  if (BACKFILL_LIMIT === 0 || Date.now() - lastGapFill < 15_000) return;
  lastGapFill = Date.now();
  try {
    const rows = await withRetry("GET /tokens/multi/graduated", () =>
      client.getGraduatedTokens({ limit: BACKFILL_LIMIT, reduceSpam: true }),
    );
    const before = graduated.size;
    for (const row of rows) onGraduated(row, "rest");
    console.log(`[rest] checked ${rows.length} graduated tokens, ${graduated.size - before} new Pump.fun rows`);
  } catch (error) {
    console.warn(`[rest] gap fill skipped: ${error instanceof Error ? error.message : String(error)}`);
  }
}

ds.on("connected", () => {
  console.log("[datastream] connected");
  void gapFill();
});
ds.on("reconnecting", (attempt: number) => console.warn(`[datastream] reconnecting, attempt ${attempt + 1}`));
ds.on("error", () => console.error("[datastream] connection error (check key and plan)"));

const curve = ds.subscribe.curvePercentage("pumpfun", CURVE_THRESHOLD).on(onCurve);
const done = ds.subscribe.graduated().on((item) => onGraduated(item, "live"));

// Drop watchlist entries that never graduated (6 h) and cap the reported set.
const pruneTimer = setInterval(() => {
  const cutoff = Date.now() - 6 * 3_600_000;
  for (const [mint, at] of watchlist) if (at < cutoff) watchlist.delete(mint);
  for (const mint of graduated) {
    if (graduated.size <= 20_000) break;
    graduated.delete(mint);
  }
}, 60_000);

process.once("SIGINT", () => {
  clearInterval(pruneTimer);
  curve.unsubscribe();
  done.unsubscribe();
  ds.disconnect();
  process.exit(0);
});

The SDK reconnects with capped exponential backoff and rejoins both rooms on its own; the code only reacts to connected. The gap fill is throttled because a rejected key or flaky network can reconnect every second, and the REST client has no built-in timeout or retry, so both are added around the call.

Reconcile after downtime

GET /tokens/multi/graduated rows carry the token and its pools but no graduation timestamp, so treat them as "graduated recently" and deduplicate by mint. reduceSpam filters out quick-graduated spam, which is usually what an alerting system wants.

When you need exact times, for example to rebuild a day of history after an outage, use search instead:

const res = await client.searchTokens({
  launchpad: "pumpfun",
  status: "graduated",
  minGraduatedAt: Date.now() - 6 * 3_600_000, // Unix ms
  sortBy: "createdAt",
  sortOrder: "desc",
  limit: 100,
});
for (const row of res.data) console.log(row.mint, row.symbol, row.graduatedAt, row.market);

Each result includes graduatedAt in Unix milliseconds (null when not graduated), launchpad.name for the origin, and market for where the token trades now. Page with page or the returned cursor. This is also the cleanest answer to "when did this specific mint graduate": search for the mint with status=graduated and read graduatedAt, because GET /tokens/{mint} has no graduation time field.

Production pitfalls

  • Treating a curve alert as a graduation. Curves stall and reverse. Expire watchlist entries (the example uses six hours) instead of assuming every alert ends in migration.
  • Assuming one destination market. Check for any non-curve pool, and prefer the newest or deepest, rather than matching a single market string.
  • Trading on the event. A graduated message means a pool exists, not that your order will fill at a given price. Request a fresh quote first; the Raptor swap API routes both bonding-curve and migrated liquidity.
  • Missing graduations during deploys. Always reconcile over REST on reconnect, and persist the reported set if restarts must not re-alert.
  • Unbounded state. Cap the reported-mints set and prune the watchlist on a timer.
  • Logging the stream URL. It contains your key.

For the earlier stage of the lifecycle, see streaming Pump.fun launches with the WebSocket API. After a token graduates, the Pump.fun first buyers API guide shows who bought early and whether they are still holding, and the rug risk API guide covers the risk score in the payload.

FAQ

What curve threshold should I use?

Pick it for the decision you make next. A low threshold such as 30% gives a wide, noisy shortlist; 80-95% gives fewer tokens closer to migration. Many systems subscribe to two rooms, for example 50% and 90%, and track movement between them.

Does the curve room fire again if the curve drops and rises past the threshold?

The docs describe the room as a "curve percentage reached" notification and do not specify repeat behavior. Deduplicate by mint in your handler, as the example does, so repeats cannot double-alert.

Can I get only Pump.fun tokens from the graduated room?

Not with a room parameter in the current SDK. The graduated room spans every launchpad, so filter by your watchlist, the presence of a "pumpfun" pool, or createdOn. Over REST you can filter by origin with GET /search?launchpad=pumpfun&status=graduated.

How do I find out when a token graduated?

Use GET /search with the mint and status=graduated and read graduatedAt (Unix ms). The graduated endpoint and token detail endpoint do not include a graduation timestamp.

Which plan do I need?

The Datastream rooms require Premium, Business or Enterprise. The REST endpoints work with any Data API key, within your plan's request limits.

References

  • Track Pump.fun tokens and graduations
  • Datastream: curve percentage room
  • Datastream: graduating room
  • Datastream: graduated room
  • Get graduated tokens
  • Get graduating tokens
  • Token search
  • @solana-tracker/data-api on npm

Companion project

The full example lives at solanatracker/examples/05-detect-pumpfun-graduation. It adds WATCH_TTL_HOURS and REDUCE_SPAM, labels each graduation with the evidence that matched, and prints a summary on Ctrl+C.

cp .env.example .env && npm install && npm start
View source on GitHub›Run in StackBlitz›

Runnable Node.js project — clone from GitHub, add keys to .env, then npm start. Use a private local environment for credentials. Check the companion package version before running.

Related Guides

Pump.fun WebSocket API: Stream New Token Launches
Pump.fun

Pump.fun WebSocket API: Stream New Token Launches

Read more
Pump.fun First Buyers API: Snipers, Insiders and PnL
Pump.fun

Pump.fun First Buyers API: Snipers, Insiders and PnL

Read more
Pump.fun gRPC: Stream and Decode New Token Mints
Pump.fun

Pump.fun gRPC: Stream and Decode New Token Mints

Read more

Products

  • Data API
  • Pump.fun API
  • Solana RPC
  • Dedicated Nodes
  • Yellowstone gRPC
  • Raptor Swap API
  • Enterprise

Trading

  • Swap
  • Latest Tokens
  • Trending
  • Top Gainers
  • Memescope
  • Whale Watch
  • KOL Tracker

Tools

  • Wallet Tracker
  • Rugcheck
  • PnL Leaderboard
  • KOLScan
  • Axiom Leaderboard
  • Photon Leaderboard
  • Bloom Leaderboard
  • FOMO Leaderboard
  • GMGN Leaderboard
  • Pump.fun App Leaderboard
  • Terminal Leaderboard
  • Platform Compare
  • My Positions
  • Teams

Resources

  • Developer Guides
  • Blog
  • Documentation
  • API Reference
  • Status
  • Affiliate Program — 25% recurring, uncapped
Solana TrackerSolana Tracker© 2026
Terms of ServicePrivacy PolicyContact