All Resources
Pump.fun gRPC: Stream and Decode New Token Mints
Pump.funTypeScript10 min readSolana Tracker

Pump.fun gRPC: Stream and Decode New Token Mints

Pump.fun gRPC guide: filter create transactions on the server, decode create and create_v2 with the IDL, and print mint, name, symbol and creator live.

  • pumpfun,
  • yellowstone-grpc,
  • anchor,
  • token-launches,
  • typescript

Pump.fun gRPC streaming means subscribing to Yellowstone gRPC transactions that create Pump.fun coins and decoding the create or create_v2 instruction yourself. You get the mint, bonding curve, name, symbol, metadata URI and creator at processed commitment, before any indexer has seen the token. The filter below narrows the stream on the server to creates only, and Anchor's BorshInstructionCoder decodes them from the program IDL.

This guide builds on the Yellowstone gRPC tutorial, which covers authentication, pings and reconnects in depth. Here the focus is the Pump.fun side: the filter, the two create variants, the decoder and the pitfalls specific to launches.

Pump.fun gRPC or the Datastream launch feed

Solana Tracker gives you two ways to see new Pump.fun tokens. The Pump.fun API exposes launches as indexed objects through the Datastream latest room and GET /tokens/latest; the Pump.fun WebSocket guide covers that path. Yellowstone gRPC gives you the raw transaction.

Yellowstone gRPC (this guide)Datastream latest room
What arrivesRaw create transaction you decodeParsed token, pool, curve progress and risk block
CommitmentYou choose: processed, confirmed or finalizedSet by the indexer
FieldsMint, bonding curve, name, symbol, URI, creator, signer, slotMetadata, price, liquidity, market cap, curve percentage, risk
Off-chain metadataFetch the URI yourselfParsed metadata included
Gap recoveryYour job (RPC backfill)GET /tokens/latest, pages 1 to 10
You maintainIDL updates, decoder, reconnectsAlmost nothing
AccessYellowstone gRPC plan, Business or Professional RPC, or a dedicated nodeData API Premium or higher for Datastream

Choose gRPC when instruction-level timing matters (sniping logic, your own indexer, custom fields like is_mayhem_mode) or when you already run a gRPC consumer. Choose Datastream when you want a ready token object with price and risk and do not want to maintain a decoder.

Filter creates on the server

The Pump.fun program (6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P) handles buys, sells and fee claims as well as creates, so a filter on the program alone delivers mostly trades. Both create instructions pass the mint-authority PDA, derived from the seed mint-authority: TSLvdd1pWpHVjahSpsvCXUbgwsL3JAcvokwaKt1eokM. In the bundled IDL no other instruction references that account, so requiring it drops trades before they leave the server:

transactions: {
  pumpCreates: {
    accountInclude: ["6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P"],
    accountRequired: ["TSLvdd1pWpHVjahSpsvCXUbgwsL3JAcvokwaKt1eokM"],
    accountExclude: [],
    vote: false,
    failed: false,
  },
},

The filter is a coarse cut, not proof. Always decode the instruction before treating a transaction as a launch.

create and create_v2

The IDL defines two create instructions. Both take name, symbol, uri and creator, and both put the new mint at account 0 and the bonding curve at account 2. They differ in token program and accounts:

createcreate_v2
Discriminator[24, 30, 200, 40, 5, 28, 7, 119][214, 144, 76, 236, 95, 139, 49, 180]
Token programSPL TokenToken-2022
Accounts14, including Metaplex metadata16, including mayhem-mode accounts
Extra argumentnoneis_mayhem_mode: bool
Signer (user) position75

Every create we sampled on mainnet in October 2026 was create_v2, but keep create in the parser: older tooling and third-party launchers still call it. Read discriminators and account positions from the IDL instead of hard-coding them, so an IDL update moves them with it.

Creates also arrive through other programs. Launch bots and launchpads call Pump.fun by CPI, so the create sits in innerInstructions, not the top-level list. Walk both.

Decode new mints in TypeScript

Set up the project and copy idl/pumpfun.json from the companion project next to index.ts as pumpfun.json:

npm init -y && npm pkg set type=module
npm install @triton-one/yellowstone-grpc@^8.0.0 @coral-xyz/anchor@^0.32.1 bs58@^6.0.0
npm install --save-dev tsx typescript @types/node
node --env-file=.env --import tsx index.ts
# Yellowstone gRPC endpoint: https://grpc.solanatracker.io (EU) or https://grpc-us.solanatracker.io (US)
YELLOWSTONE_GRPC_ENDPOINT=
# x-token from https://www.solanatracker.io/account/yellowstone-grpc
YELLOWSTONE_GRPC_TOKEN=
# processed (fastest, can roll back), confirmed or finalized
COMMITMENT=processed
import Client, { CommitmentLevel, type SubscribeRequest, type SubscribeUpdate } from "@triton-one/yellowstone-grpc";
import { BorshInstructionCoder, type Idl } from "@coral-xyz/anchor";
import bs58 from "bs58";
import idl from "./pumpfun.json" with { type: "json" };

function required(name: string): string {
  const value = process.env[name]?.trim();
  if (!value) {
    console.error(`Missing ${name} in .env`);
    process.exit(1);
  }
  return value;
}
const endpoint = required("YELLOWSTONE_GRPC_ENDPOINT");
const token = required("YELLOWSTONE_GRPC_TOKEN");
const LEVELS: Record<string, CommitmentLevel> = {
  processed: CommitmentLevel.PROCESSED,
  confirmed: CommitmentLevel.CONFIRMED,
  finalized: CommitmentLevel.FINALIZED,
};
const commitment = LEVELS[(process.env.COMMITMENT?.trim() || "processed").toLowerCase()];
if (commitment === undefined) {
  console.error("COMMITMENT must be processed, confirmed or finalized");
  process.exit(1);
}

const PUMP_PROGRAM = idl.address;
const PUMP_MINT_AUTHORITY = "TSLvdd1pWpHVjahSpsvCXUbgwsL3JAcvokwaKt1eokM"; // PDA of "mint-authority"
const coder = new BorshInstructionCoder(idl as Idl);
const variants = ["create", "create_v2"].map((name) => {
  const ix = idl.instructions.find((i) => i.name === name);
  if (!ix) throw new Error(`${name} is missing from pumpfun.json`);
  return { name, discriminator: Buffer.from(ix.discriminator), mint: ix.accounts.findIndex((a) => a.name === "mint") };
});

const empty = { accounts: {}, slots: {}, transactions: {}, transactionsStatus: {}, blocks: {}, blocksMeta: {}, entry: {}, blockFooter: {}, accountsDataSlice: [], commitment };
const subscription: SubscribeRequest = {
  ...empty,
  transactions: {
    pumpCreates: { accountInclude: [PUMP_PROGRAM], accountRequired: [PUMP_MINT_AUTHORITY], accountExclude: [], vote: false, failed: false },
  },
};

type TxInfo = NonNullable<NonNullable<SubscribeUpdate["transaction"]>["transaction"]>;

// Top-level and inner instructions in execution order, with account indexes resolved to base58.
function* instructions(info: TxInfo) {
  const message = info.transaction?.message;
  const meta = info.meta;
  if (!message || !meta) return;
  const keys = [...message.accountKeys, ...meta.loadedWritableAddresses, ...meta.loadedReadonlyAddresses].map((k) => bs58.encode(k));
  const inner = new Map(meta.innerInstructions.map((group) => [group.index, group.instructions]));
  const resolve = (ix: { programIdIndex: number; accounts: Uint8Array; data: Uint8Array }, path: string) => ({
    path,
    programId: keys[ix.programIdIndex],
    accounts: [...ix.accounts].map((index) => keys[index] ?? "?"),
    data: Buffer.from(ix.data),
  });
  for (const [i, ix] of message.instructions.entries()) {
    yield resolve(ix, `${i}`);
    for (const [j, cpi] of (inner.get(i) ?? []).entries()) yield resolve(cpi, `${i}.${j}`);
  }
}

// Names and symbols are user input: strip control and bidi characters, cap the length.
const clean = (value: unknown, max: number) =>
  String(value).replace(/[\u0000-\u001f\u007f-\u009f​-‏‪-‮]/g, "").trim().slice(0, max);

const seen = new Set<string>();
function onTransaction(info: TxInfo, slot: string): void {
  if (info.meta?.err) return;
  for (const ix of instructions(info)) {
    if (ix.programId !== PUMP_PROGRAM) continue;
    const variant = variants.find((v) => ix.data.subarray(0, 8).equals(v.discriminator));
    if (!variant) continue;
    const decoded = coder.decode(ix.data);
    if (decoded?.name !== variant.name) {
      console.warn(`${variant.name} did not decode; update pumpfun.json`);
      continue;
    }
    const key = `${bs58.encode(info.signature)}:${ix.path}`;
    if (seen.has(key)) continue;
    seen.add(key);
    if (seen.size > 10_000) seen.clear();
    const args = decoded.data as Record<string, unknown>; // { name, symbol, uri, creator: PublicKey, ... }
    console.log(
      [slot, variant.name, ix.accounts[variant.mint], clean(args.symbol, 10), clean(args.name, 32), String(args.creator)].join("  "),
    );
  }
}

let stopping = false;
let active: Awaited<ReturnType<Client["subscribe"]>> | undefined;

async function streamOnce(onHealthy: () => void): Promise<void> {
  const client = new Client(endpoint, token, undefined);
  await client.connect();
  const stream = await client.subscribe();
  active = stream;
  console.log("[grpc] connected");
  await new Promise<void>((resolve, reject) => {
    const closed = () => (stopping ? resolve() : reject(new Error("stream closed by server")));
    stream.on("data", (update: SubscribeUpdate) => {
      onHealthy();
      if (update.ping) return void stream.write({ ...empty, ping: { id: 1 } });
      const tx = update.transaction;
      if (tx?.transaction) onTransaction(tx.transaction, tx.slot);
    });
    stream.on("error", reject);
    stream.on("end", closed);
    stream.on("close", closed);
    stream.write(subscription);
  });
}

process.on("SIGINT", () => {
  stopping = true;
  active?.destroy();
  process.exit(0);
});

let attempt = 0;
while (!stopping) {
  try {
    await streamOnce(() => (attempt = 0));
  } catch (error) {
    attempt++;
    const delay = Math.min(30_000, 500 * 2 ** (attempt - 1)) * (0.5 + Math.random() / 2);
    console.warn(`[grpc] ${error instanceof Error ? error.message : error}; reconnecting in ${Math.round(delay)} ms`);
    await new Promise((resolve) => setTimeout(resolve, delay));
  }
}

Each line prints the slot, variant, mint, symbol, name and creator. The decoder returns creator as an Anchor PublicKey, which stringifies to base58.

What the decoder returns

coder.decode(data) matches the 8-byte discriminator against the IDL and returns the instruction name and its arguments. For a create_v2 the decoded value looks like this (illustrative values):

{
  "name": "create_v2",
  "data": {
    "name": "Example Coin",
    "symbol": "EXAMPLE",
    "uri": "https://example.invalid/metadata.json",
    "creator": "9pQrExampleCreatorAddressxxxxxxxxxxxxxxxxx",
    "is_mayhem_mode": false
  }
}

The mint and bonding curve are not arguments. Read them from the instruction's account list at the positions the IDL gives (mint at 0, bondingCurve at 2), after resolving lookup-table addresses. creator is an argument and the signer is the user account; in the creates we sampled they were the same wallet, but a launcher can set a different creator, so keep both.

The program also emits a CreateEvent with reserves, token program and mayhem flags. Decoding it requires the event log or event CPI; instruction decoding works even when logs are truncated, which is why this guide uses it.

Production pitfalls

  • Processed creates can disappear. Processed is the fastest level and the default here, but a skipped slot drops a launch you already printed. Re-check against confirmed data before you store it as fact, or set COMMITMENT=confirmed.
  • Duplicates. Reconnects and processed redelivery can repeat a transaction. Deduplicate on signature plus instruction path, as above, since one transaction can create more than one coin.
  • Failed transactions. failed: false drops most of them server-side; the meta.err check catches the rest.
  • IDL drift. When Pump.fun ships a new create variant, the discriminator check misses it silently. Log decode failures and unmatched instructions that touch the mint-authority PDA, and update the IDL when they appear.
  • Untrusted metadata. Names, symbols and URIs are attacker-controlled. Strip control characters, cap length, and fetch the URI through a worker with timeouts, response-size limits and private-network blocking, never from the stream handler.
  • Slow handlers. Launch bursts arrive in clusters. Do the decode inline and push anything slower (metadata fetches, database writes) to a queue.

After a launch, the next questions are usually who bought first and when it graduates: see Pump.fun first buyers for the former.

FAQ

Why filter on the mint-authority PDA instead of the program?

The program filter delivers every buy and sell. The PDA appears only in create instructions in the current IDL, so accountRequired cuts the stream down to launches on the server and saves bandwidth and decode work.

Do I need both create and create_v2?

Yes. Current launches use create_v2 (Token-2022), but create is still a valid instruction. Supporting both costs one extra discriminator.

Can I get the token image and socials from gRPC?

Not directly. The transaction carries the metadata uri; fetch it yourself through a controlled worker. The Datastream latest room returns parsed metadata if you prefer not to.

Which commitment should I use for new Pump.fun mints?

Processed for the earliest signal, accepting that some launches will roll back. Confirmed if you store or alert on every launch and can afford a short delay.

How do I backfill launches missed during a disconnect?

Track block parent links as in the setup guide and fetch the missing blocks over RPC, or reconcile against GET /tokens/latest from the Pump.fun API, which returns up to 100 tokens per page across 10 pages.

References

Companion project

The full example lives at solanatracker/examples/08-pumpfun-stream-new-minted-tokens. It bundles idl/pumpfun.json, derives layouts from it, prints an aligned table with the calling program for CPI creates, and exits cleanly on Ctrl+C.

cp .env.example .env && npm install && npm start

Runnable Node.js project — clone from GitHub, add keys to .env, then npm start. gRPC examples use native dependencies — run locally with Node.js.

Build on this example

Ask for changes, a different stack or the next feature. Answers use the Data API SDK and link the docs.