Skip to main content

Monitor Solana Programs with Yellowstone Geyser gRPC (TypeScript)

Updated on
Aug 20, 2025

20 min read

Updates: fromSlot Parameter for Historical Replay and more

As of July 2025, QuickNode has enabled the fromSlot parameter to enable clients to replay data from up to 3000 recent slots - around 20 minutes, providing improved flexibility for clients that may experience network instability or need backfill during recovery.

This guide has been updated to reflect this new feature, we well as updated to use Solana Kit, current versions of Node.js, and other newer tools.

Overview

In this guide, we'll learn about Solana Geyser plugins and explore how to use Yellowstone gRPC, a powerful Geyser plugin for Solana, to monitor real-time on-chain activity. Specifically, we'll create a TypeScript application that tracks new token mints from the Pump.fun program on Solana's mainnet. This project will demonstrate how to leverage Geyser's low-latency data access capabilities to build responsive and efficient monitoring tools.

Prefer a visual format? Follow along the video to learn how to monitor Solana program data using Yellowstone gRPC Add-on in 9 minutes.
Subscribe to our YouTube channel for more videos!

What You Will Do

  • Learn about Geyser and Yellowstone gRPC
  • Use TypeScript and Yellowstone to monitor new Pump.fun mints on Solana
  • Be able to reapply this logic to other programs

Here's what the program will look like:

Yellowstone Output

What You Will Need

What is Geyser?

Geyser is a plugin system for Solana validators that provides low-latency access to blockchain data without overloading validators with intensive RPC requests (e.g., getProgramAccounts). Instead of querying the validator directly, Geyser plugins stream real-time information about accounts, transactions, slots, and blocks to your chosen external data store, for example a relational database, NoSQL database, or streaming platforms like Kafka. This approach significantly reduces the load on validators while improving data access efficiency.

The key advantage of Geyser plugins is their ability to scale with high-volume Solana applications. By routing data queries to external stores, developers can implement optimized access patterns like caching and indexing, which is particularly valuable for applications requiring frequent access to large datasets or historical information. This separation allows validators to focus on their primary role of processing transactions while ensuring developers have the comprehensive, real-time data access they need.

What is Yellowstone Geyser gRPC?

Yellowstone Geyser gRPC (commonly referred to as "Yellowstone", or sometimes "Yellowstone Dragon's Mouth") is an open source gRPC interface built on Solana's Geyser plugin system. It leverages gRPC, Google's high-performance framework that combines Protocol Buffers for serialization with HTTP/2 for transport, enabling fast and type-safe communication between distributed systems.

Yellowstone provides real-time streaming of:

  • Account updates
  • Transactions
  • Entries
  • Block notifications
  • Slot notifications

Compared to traditional WebSocket implementations, Yellowstone's gRPC interface offers lower latency and higher stability. It also includes unary operations for quick, one-time data retrievals. The combination of gRPC's efficiency and type safety makes Yellowstone particularly well-suited for cloud-based services and database updates. QuickNode supports Yellowstone through our Yellowstone Geyser gRPC Marketplace Add-on.

Let's see Yellowstone in action by writing a script to monitor new Pump.fun mints on Solana.


Performance Considerations for High-Volume Data Streams

While this guide demonstrates Yellowstone gRPC using TypeScript, if you're building systems that need to process all transactions from busy programs or track multiple programs simultaneously, consider using our Yellowstone gRPC on Rust or Yellowstone gRPC on Go guides.

Create a New Project

Let's start by setting up a new TypeScript project:

  1. Create a new directory for your project and navigate into it:

    mkdir pump-fun-monitor && cd pump-fun-monitor
  2. Initialize a new Node.js project:

    npm init -y

Then enable ES6 modules in your package.json file by adding the following:

{
...existing package.json content...
"type": "module"
}
  1. Install the required dependencies:

    npm install tsx @triton-one/yellowstone-grpc @types/node @solana/kit
  2. Save the IDL for the program you want to monitor. In this case, we'll get the IDL for the Pump.fun program.

    curl -o program.json https://raw.githubusercontent.com/rckprtr/pumpdotfun-sdk/refs/heads/main/src/IDL/pump-fun.json

Now you're ready to start writing your program!

Write the Script

Let's create our program to monitor Pump.fun mints using Yellowstone. We'll break this down into several steps:

Step 1: Define Interfaces

We'll mostly be using the interfaces from Yellowstone, so we don't need to define many of them here. Create a new file called lib/interfaces.ts and add the following code:

export interface CompiledInstruction {
programIdIndex: number;
accounts: Uint8Array;
data: Uint8Array;
}

export interface MintInformation {
mint: string;
transaction: string;
slot: number;
}

Step 2: Create a function to get your Yellowstone endpoint

Yellowstone uses a different port than the usual RPC endpoint. This function will convert your QuickNode RPC endpoint to a Yellowstone endpoint and token. Create a new file called lib/quicknode.ts and add the following code:

// Convert the RPC endpoint to a Yellowstone endpoint and token
export const getYellowstoneEndpointAndToken = (rpcEndpoint: string) => {
// Convert endpoint to URL object
const url = new URL(rpcEndpoint);
const YELLOWSTONE_PORT = 10000;

// Yellowstone endpoint is the same as the RPC endpoint, but with the port 10000 and no pathname
const yellowstoneEndpoint = `${url.protocol}//${url.hostname}:${YELLOWSTONE_PORT}`;

// The token is the pathname of the RPC endpoint, but without the leading slash
const yellowstoneToken = url.pathname.replace(/\//g, "");

return { yellowstoneEndpoint, yellowstoneToken };
};

Step 3: Create helper functions

Yellowstone returns transaction signatures and token addresses as Uint8Array. We need to convert them to base58 to make them readable. Create a new file called lib/helpers.ts and add the following code:

import { getBase58Decoder } from "@solana/kit";

export const bufferToBase58 = (buffer: Uint8Array): string => {
return getBase58Decoder().decode(buffer);
};

Next, we'll add a function to get the explorer URL for an address or transaction. Modern terminal apps support clicking links, so it will allow us to see each mint as it's created:

export const getExplorerUrl = (address: string, type: "address" | "tx") => {
return `https://explorer.solana.com/${type}/${address}`;
};

Next we'll add a function to get the discriminator for an instruction handler from a program's IDL. We can use this later to find instructions that called this instruction handler.

export const getInstructionHandlerDiscriminator = (
programIdl: any,
instructionName: string
) => {
const instruction = programIdl.instructions.find(
(instruction: any) => instruction.name === instructionName
);
const discriminatorBytes = instruction.discriminator;
return Buffer.from(discriminatorBytes);
};

Next we'll add a function to get the account names and indices for the accounts used in an instruction handler. We can use this later to filter transactions to only include instructions for a particular instruction handler.

export const getAccountsFromIdl = (
programIdl: any,
instructionName: string
): Array<{ name: string; index: number }> => {
const instruction = programIdl.instructions.find(
(instruction: any) => instruction.name === instructionName
);

if (!instruction) {
throw new Error(`Instruction '${instructionName}' not found in IDL`);
}

return instruction.accounts.map((account: any, index: number) => ({
name: account.name,
index: index,
}));
};

Step 4: Create constants

A few things don't change so often, so let's define them. Create a new file called lib/constants.ts and add the following code:

// Set by Solana
export const SOLANA_SLOT_TIME_MS = 400;

// Set by QuickNode
export const MAX_SLOTS_TO_REPLAY = 3000;

// Set by the late 18th century French scientists
export const SECONDS = 1000;

// Set by ancient Babylonians
export const MINUTES = SECONDS * 60;

export const MAX_TIME_TO_REPLAY_MS = MAX_SLOTS_TO_REPLAY * SOLANA_SLOT_TIME_MS;
export const MAX_TIME_TO_REPLAY_MINUTES = MAX_TIME_TO_REPLAY_MS / 1000 / 60;

Step 5: Create our Yellowstone client

Create a new file called lib/yellowstone.ts. We'll make this as generic as possible so you can reuse this across multiple projects. Start by importing the relevant dependencies - Yellowstone, gRPC, and our helpers, interfaces, and constants.

import Client, {
CommitmentLevel,
SubscribeRequest,
SubscribeUpdate,
SubscribeUpdateTransaction,
} from "@triton-one/yellowstone-grpc";
import { ClientDuplexStream } from "@grpc/grpc-js";
import { bufferToBase58, getExplorerUrl } from "./helpers";
import { CompiledInstruction, MintInformation } from "./interfaces";
import {
MAX_SLOTS_TO_REPLAY,
MAX_TIME_TO_REPLAY_MINUTES,
SOLANA_SLOT_TIME_MS,
} from "./constants";

Let's make a couple of handy Yellowstone-specific functions. Yellowstone natively returns the slot as a string, so let's fix that:

export const getCurrentSlot = async (
yellowstoneClient: Client
): Promise<number> => {
const currentSlotString = await yellowstoneClient.getSlot();
return Number(currentSlotString);
};

Create a Yellowstone subscription request

This function will create a Yellowstone SubscribeRequest that will monitor the given program IDs and required accounts.

We want to be as specific as possible with our filters, so we can reduce the amount of data we are receiving from the server:

  • accountInclude: include transactions that use any account from the array.
  • accountExclude: exclude transactions that use any account from the array.
  • accountRequired: only include transactions that use all accounts from the array.

We prefer to use accountRequired as it's more specific than accountInclude and accountExclude.

export const createSubscribeRequest = (
includedAccounts: Array<string>,
excludedAccounts: Array<string>,
requiredAccounts: Array<string>,
fromSlot: number | null = null
): SubscribeRequest => {
// See https://github.com/rpcpool/yellowstone-grpc?tab=readme-ov-file#filters-for-streamed-data for full list of filters.
const request: SubscribeRequest = {
commitment: CommitmentLevel.CONFIRMED,
accounts: {},
slots: {},
transactions: {
// We can have multiple filters here, but for this demo, we'll only have one.
// When we get events, we can check which filter was matched.
// https://github.com/rpcpool/yellowstone-grpc?tab=readme-ov-file#transactions
pumpFun: {
vote: false,
failed: false,
accountInclude: includedAccounts,
accountExclude: excludedAccounts,
accountRequired: requiredAccounts,
},
},
transactionsStatus: {},
entry: {},
blocks: {},
blocksMeta: {},
accountsDataSlice: [],
ping: undefined,
};

if (fromSlot) {
// Yellowstone expects the slot as a string, so let's fix that.
request.fromSlot = String(fromSlot);
}

return request;
};

Let's also make a function to send the subscription request to the Yellowstone stream - we'll make the stream in a moment.

export const sendSubscribeRequest = (
stream: ClientDuplexStream<SubscribeRequest, SubscribeUpdate>,
request: SubscribeRequest
): Promise<void> => {
return new Promise<void>((resolve, reject) => {
stream.write(request, (error: Error | null) => {
if (error) {
reject(error);
} else {
resolve();
}
});
});
};

Add some client-side filters

Most of our filtering is done through YellowStone, but Yellowstone doesn't directly filter transactions by instruction handler, so we'll add an additional client-side checks:

// Yellowstone gRPC doesn't directly filter by instruction handler so we have to
// do it client-side.
export const checkInstructionMatchesInstructionHandlers = (
instruction: CompiledInstruction,
instructionHandlerDiscriminators: Array<Uint8Array>
): boolean => {
return (
instruction?.data &&
instructionHandlerDiscriminators.some((instructionHandlerDiscriminator) =>
Buffer.from(instructionHandlerDiscriminator).equals(
instruction.data.slice(0, 8)
)
)
);
};

We'll also create a function to make a nice Object of name/address pairs, so we can get the addresses this instruction used for a specific account names (like the mint account of every new token on Pump.fun) and display them in a nice table.

export const getAccountsByName = (
accountsToInclude: Array<{ name: string; index: number }>,
instruction: CompiledInstruction,
accountKeys: Array<Uint8Array>
): Record<string, string> => {
return accountsToInclude.reduce<Record<string, string>>(
(accumulator, account) => {
const accountIndex = instruction.accounts[account.index];
const address = bufferToBase58(accountKeys[accountIndex]);
accumulator[account.name] = address;
return accumulator;
},
{}
);
};

Done! Now let's start handing the updates we get from Yellowstone, using the functions we've just created.

Make a function to turn SubscribeUpdate into nicely formatted mint information

We'll create a function that will convert our Yellowstone SubscribeUpdate to a MintInformation - a simple object with the mint address, transaction signature, and slot number.

export const getMintInfoFromUpdate = (
update: SubscribeUpdate,
instructionHandlerDiscriminators: Array<Uint8Array>,
accountsToInclude: Array<{ name: string; index: number }>
): null | MintInformation => {
// Check the filter name that was matched
// (Yellowstone also sends other things like 'ping' updates, but we don't care about those)
if (!update.filters.includes("pumpFun")) {
return null;
}

// These should never happen in this demo,
// since our filter's matches will include the right properties.
// but let's satisfy the type checker.
const transaction = update.transaction?.transaction;
const message = transaction?.transaction?.message;
const slot = update.transaction?.slot;
if (!transaction || !message || !slot) {
return null;
}

// Find the instruction that matches our target instruction handler
const instruction =
message.instructions.find((instruction) =>
checkInstructionMatchesInstructionHandlers(
instruction,
instructionHandlerDiscriminators
)
) || null;
if (!instruction) {
return null;
}

// Make a nice Object of account value/address pairs, so we can get the address
// values this instruction used for each account name.
const accountsByName = getAccountsByName(
accountsToInclude,
instruction,
message.accountKeys
);

const base58TransactionSignature = bufferToBase58(transaction.signature);

return {
mint: getExplorerUrl(accountsByName.mint, "address"),
transaction: getExplorerUrl(base58TransactionSignature, "tx"),
slot: Number(slot),
};
};

Connect the stream to our functions

Now we're ready to connect the stream to our functions. Whenever the stream sends an update, we'll call getMintInfoFromUpdate to get the mint information, and print it to the console. Open lib/yellowstone.ts and add the following code:


export const handleStreamEvents = (
stream: ClientDuplexStream<SubscribeRequest, SubscribeUpdate>,
instructionDiscriminators: Array<Uint8Array>,
accountsToInclude: Array<{ name: string; index: number }>
): Promise<void> => {
return new Promise<void>((resolve, reject) => {
stream.on("data", (update: SubscribeUpdate) => {
const mintInfo = getMintInfoFromUpdate(
update,
instructionDiscriminators,
accountsToInclude
);

if (mintInfo) {
console.log("💊 New Pump.fun Mint Detected!");
console.table(mintInfo);
console.log("\n");
}
});
stream.on("error", (error: Error) => {
console.error("Stream error:", error);
reject(error);
stream.end();
});
stream.on("end", () => {
console.log("Stream ended");
resolve();
});
stream.on("close", () => {
console.log("Stream closed");
resolve();
});
});
};

That's all of yellowstone.ts!

Step 6: Choosing our filters and putting it all together

Now we're ready to create the main script. Here we will set a bunch of values for the transactions we want to monitor, we'll connect to YellowStone, and then call the functions we've just created to monitor the transaction.

We'll want to be as specific as possible with our filters, so we can reduce the amount of data we are receiving from the server, use less API credits, and make our script more efficient. We could just include PROGRAM_ID to get all transactions involving the program, but since we are only looking for a subset of instructions (in this case, create instructions), we could look at the IDL and identify any additional accounts that might be only passed into the create instruction handler. Since the Pump.fun Token Mint Authority is used in every create instruction handler, so by requiring both accounts we can reduce the amount of data we are receiving and make our script more efficient.

We'll also want to show the specific address used for 'mint'. Solana instructions use arrays, not named accounts, so we need to get the index of the account we want to watch from the program's IDL like we did earlier. Feel free to include additional accounts you want to monitor (you can get their index from your program's IDL).

Create a new file called monitor-program.ts and add the following code:

import {
createSubscribeRequest,
handleStreamEvents,
sendSubscribeRequest,
getSlotFromTimeAgo,
} from "./lib/yellowstone";
import { getYellowstoneEndpointAndToken } from "./lib/quicknode";
import { env } from "node:process";
import { MINUTES } from "./lib/constants";
import Client from "@triton-one/yellowstone-grpc";
import {
getInstructionHandlerDiscriminator,
getAccountsFromIdl,
} from "./lib/helpers";
import programIdl from "./program.json";

// We're watching the pump.fun program
const PROGRAM_ID = programIdl.address;

// We're watching the create() instruction handler
const PUMP_FUN_CREATE_INSTRUCTION_HANDLER_DISCRIMINATOR =
getInstructionHandlerDiscriminator(programIdl, "create");

const PUMP_FUN_MINT_AUTHORITY = "TSLvdd1pWpHVjahSpsvCXUbgwsL3JAcvokwaKt1eokM";

// The program and required accounts to watch via Yellowstone gRPC
// See https://github.com/rpcpool/yellowstone-grpc?tab=readme-ov-file#filters-for-streamed-data for full list of filters.
const requiredAccounts: Array<string> = [PROGRAM_ID, PUMP_FUN_MINT_AUTHORITY];

// After we get the events from Yellowstone gRPC, we'll filter them by the instruction handler (onchain function) being invoked
const instructionDiscriminators: Array<Uint8Array> = [
PUMP_FUN_CREATE_INSTRUCTION_HANDLER_DISCRIMINATOR,
];

// Get account information from the IDL for the create instruction
// This will include all accounts used in the create instruction with their names and indices
const ACCOUNTS_TO_INCLUDE = getAccountsFromIdl(programIdl, "create");

const rpcEndpoint = env["QUICKNODE_SOLANA_MAINNET_ENDPOINT"];
if (!rpcEndpoint) {
throw new Error(
"QUICKNODE_SOLANA_MAINNET_ENDPOINT environment variable is required"
);
}

const { yellowstoneEndpoint, yellowstoneToken } =
getYellowstoneEndpointAndToken(rpcEndpoint);

const yellowstoneClient = new Client(yellowstoneEndpoint, yellowstoneToken, {});
// Somewhat confusingly, we need to call `subscribe` on the client to get a stream
// and then make a subscribe request to the stream.
const stream = await yellowstoneClient.subscribe();

// We'll use this later in the guide
const fromSlot = null;

// Create subscribe request with fromSlot parameter
const request = createSubscribeRequest([], [], requiredAccounts, fromSlot);

await sendSubscribeRequest(stream, request);
console.log(
"🔌 Geyser connection established - watching new Pump.fun token mints...\n"
);
await handleStreamEvents(
stream,
instructionDiscriminators,
ACCOUNTS_TO_INCLUDE
);

Add your QuickNode endpoint to an .env file

Before running the script, getting your endpoint from the QuickNode dashboard and add it to a new file called .env, replacing the endpoint with your own:

QUICKNODE_SOLANA_MAINNET_ENDPOINT="https://three-custom-words.solana-mainnet.quiknode.pro/1234567890abcdefghijklmnopqrstuvwxyz1234/"

Don't commit your .env file to your repository!

Don't commit your .env file to your repository, as it contains your QuickNode endpoint. This is a security risk, as it allows anyone to access your endpoint and use it without your permission. Add it to your .gitignore file to avoid this.

Run the script

Now we're ready to run the script. In your terminal, run the following command:

npx tsx --env-file=.env monitor-program.ts

You should see the following output:

npx tsx --env-file=.env monitor-program.ts 
🔌 Geyser connection established - watching new Pump.fun token mints...

💊 New Pump.fun Mint Detected!
┌─────────────┬───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│ (index) │ Values │
├─────────────┼───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
│ mint │ 'https://explorer.solana.com/address/G8XYfdnujEiwivG8LZuj5NKppUkx6nn6Wo72A1Ckpump' │
│ transaction │ 'https://explorer.solana.com/tx/2sSHoWvNNuHVMJMPLeramPNKj8VJvtjhG6hLrci4FmnYG6xmPEqRiKHAWY15wcTGMxqhgnLi8DRBPWEM3r5bSAut' │
│ slot │ 358713246 │
└─────────────┴───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘


💊 New Pump.fun Mint Detected!
┌─────────────┬───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│ (index) │ Values │
├─────────────┼───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
│ mint │ 'https://explorer.solana.com/address/EjrHSwxfZ3mCbLnnuXg4xYuHuzvMCbB5S4ptVzxAfonq' │
│ transaction │ 'https://explorer.solana.com/tx/2rR47UGUcv5EY5hTij8eVshZPHqLV2mUVJdZYr4kxm25EQwLdumqt3v9RfLMfGBM7ENVpRGbdD1b3Q7L4mo3W9TX' │
│ slot │ 358713253 │
└─────────────┴───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘


💊 New Pump.fun Mint Detected!
┌─────────────┬───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│ (index) │ Values │
├─────────────┼───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
│ mint │ 'https://explorer.solana.com/address/8prBRtMZvYpppiUfcKbmteQx8gHqn8XicDZAPfPRpump' │
│ transaction │ 'https://explorer.solana.com/tx/5cS6buykKeFaE9bNDzoo8PvKixew4cV14hJMMLYSwuvQm5xWrxr2uRf2Hf3ZRrwizKbc8GsytmbxbpaXhHgYaZYJ' │
│ slot │ 358713276 │
└─────────────┴───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘

You can click on the mint address to see the mint information on Solana Explorer:

Mint Information in Solana Explorer

Have problems? The full code is available on the pump.fun monitor GitHub repository.

Historical Replay with fromSlot

Sometimes we want to resume from a given time in the past - for clients that may experience missed messages or need backfill during recovery. We can also resume from any point in time in the last 3000 slots (20 minutes).

We would normally save the most recent slot number as new slots come in, and set fromSlot to that number in the subscription request when recovering.

For this demo we'll just pick a slot from a particular time ago - open lib/yellowstone.ts and add the following function:

// Allow us to get the slot from a given time in the past
export const getSlotFromTimeAgo = async (
yellowstoneClient: Client,
timeAgo: number
) => {
const now = Date.now();
const fromTime = now - timeAgo;

const slotsAgo = Math.ceil(timeAgo / SOLANA_SLOT_TIME_MS);
if (slotsAgo > MAX_SLOTS_TO_REPLAY) {
throw new Error(
`From time ${new Date(
fromTime
).toISOString()} is too far in the past. Maximum time to replay is ${MAX_TIME_TO_REPLAY_MINUTES} minutes.`
);
}

const currentSlot = await getCurrentSlot(yellowstoneClient);
const fromSlot = currentSlot - slotsAgo;

return fromSlot;
};

Then in monitor-program.ts, we can set fromSlot to the slot 5 minutes ago:

// Typically, we would record the most recent slot when each event is recieved,
// and then replay from that slot if we need to recover.
// For this demo, let's get the last 5 minutes of transactions
const fromSlot = await getSlotFromTimeAgo(yellowstoneClient, 5 * MINUTES);

Re-run the script and you should see a burst of historical data before catching up to real-time. You can click on any of the mint or transactions to see the details on Solana Explorer including the time ago.

Wrap Up

In this guide, we've explored how to use Yellowstone, a powerful Geyser plugin, to monitor Solana programs in real-time. We focused on tracking new token mints from the Pump.fun program, but the principles we've covered can be used to monitor any Solana program or account.

As you continue to build on Solana, consider how you can leverage Geyser plugins like Yellowstone to create more responsive and efficient applications. Whether you're building a trading bot, an analytics dashboard, or a complex DeFi application, low latency real time data access can give you a significant edge.

Resources

Let's Connect!

We'd love to hear how you are using Yellowstone. Send us your experience, questions, or feedback via Twitter or Discord.

We ❤️ Feedback!

Let us know if you have any feedback or requests for new topics. We'd love to hear from you.

Share this guide