Monitoring Liquidity Pools on Solana DEXes with GetBlock's Yellowstone gRPC

Step-by-step guide for building a Node.js app to track real-time swaps on Solana using GetBlock's Yellowstone gRPC

Monitoring liquidity pools on Solana decentralised exchanges (DEXes) in real-time is crucial for traders and developers who want to stay updated with the latest market conditions and make informed decisions.

GetBlock's Yellowstone gRPC service allows developers to fetch data directly from Solana validators easily with low latency - appropriately ~400ms latency.

In this guide, you'll build a monitor that tracks every swap happening on a specific Solana DEX liquidity pool, regardless of which platform (Jupiter, Raydium, Phantom Wallet, etc.) the user is trading with.

You'll learn how to:

  • Connect to GetBlock's Yellowstone gRPC service for ultra-low latency blockchain data streaming

  • Subscribe to transactions involving specific liquidity pool addresses

  • Filter out failed transactions to show only successful swaps

  • Identify which DEX or aggregator initiated each swap (Jupiter, Raydium, etc.)

  • Display real-time swap alerts with formatted output and explorer links

  • Track comprehensive statistics about trading activity

Prerequisites

Before you begin, ensure you have:

  • Node.js and npm installed

  • Basic knowledge of JavaScript

  • A Dedicated Solana Node subscription on GetBlock - Required for Yellowstone gRPC access

Technology Stack:

  • Node.js

  • @triton-one/yellowstone-grpc - Official Yellowstone gRPC client

  • bs58 - Base58 encoding for Solana addresses

  • GetBlock’s Dedicated Solana Node with Yellowstone gRPC add-on

Step 1: Set Up Your GetBlock’s Yellowstone Endpoint

Deploy Your Dedicated Solana Node

First, you need to deploy a dedicated Solana node on GetBlock with the Yellowstone gRPC add-on enabled.

1. Sign up or log in

  • Create an account or log in to your existing account

2. Deploy your dedicated node

  • Navigate to your user Dashboard

  • Switch to the "Dedicated nodes" tab

  • Scroll down to "My endpoints"

  • Under "Protocol", select Solana

  • Set the network to Mainnet

  • Click Get

3. Enable the Yellowstone gRPC add-on

In Step 3 of your node setup (Select API and Add-ons):

  • Check the box for Yellowstone gRPC under Add-ons

  • Complete payout and finalize the setup

Generate a Yellowstone gRPC Access Token

Once your node is live, you'll create an access token to authenticate your gRPC connections.

1. Return to your dashboard

  • Under your dedicated node dashboard, click on "My endpoints".

  • Select Yellowstone gRPC

  • Click on Add to generate an access token

2. Your endpoint URL

You'll receive an HTTPS-style gRPC endpoint URL based on your chosen region:

https://go.getblock.io/YOUR_ACCESS_TOKEN/

Step 2: Set up Development Environment

  1. Create a directory for your project

mkdir pool-monitor
cd pool-monitor
npm init-y
  1. Install Dependencies:

npm install @triton-one/yellowstone-grpc [email protected]

What these packages do:

[email protected] because version 6.x + uses ES modules only, which don't work with vanilla js - require() statements.

  1. Configure Package.json

{
  "name": "pool-monitor",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "start": "node pool-monitor.js"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "type": "module",
  "dependencies": {
    "@triton-one/yellowstone-grpc": "^4.0.2",
    "bs58": "^5.0.0",
    "dotenv": "^17.2.3"
  }
}
  • "type": "module" - Enables ES6 import/export syntax

  • "scripts" - Defines shortcuts like npm start

Project Structure

Create the following files to have a basic structure for your project:

├── pool-monitor.js.          // Main application
└── .env                // Environment variables
└── .gitignore          // Git ignore file

Step 3: Start Building Your Monitor

Import Dependencies

Create a new file pool-monitor.js and add:

import Client from "@triton-one/yellowstone-grpc";
import { CommitmentLevel } from "@triton-one/yellowstone-grpc";
import bs58 from "bs58";
import { config } from "dotenv";

// Load environment variables from .env
config();

What this does:

  • Client- The main class you'll use to connect to GetBlock's Yellowstone gRPC service and create a streaming connection

  • CommitmentLevel - Solana has different levels of transaction finality (how "confirmed" a transaction is). You'll use this to choose how finalised you want transactions to be before receiving them

  • bs58 - A library that converts Solana's binary address data (which is just bytes) into the readable base58 format you see on block explorers (like 8sLbNZ...)

  • config() - To load the .env variables.

Add Your GetBlock Configuration

// GetBlock Configuration
const ENDPOINT = "https://go.getblock.io";  // Your region's endpoint
const TOKEN = process.env.GETBLOCK_TOKEN; ;           // Your generated token

What this does:

  • Stores your GetBlock credentials that you created in Step 1. Replace YOUR_ACCESS_TOKEN with the actual token you generated. If you chose a different region, use that endpoint instead (e.g., https://go.getblock.us or https://go.getblock.asia).

Add Pool Configuration

// Pool Configuration
const TARGET_POOL = "8sLbNZoA1cfnvMJLPfp98ZLAnFSYCFApfJKMbiXNLwxj"; // SOL/USDC CLMM
const POOL_NAME = "SOL/USDC";

What this does:

  • TARGET_POOL - The unique address of the liquidity pool you're monitoring. This is the SOL/USDC Concentrated Liquidity pool on Raydium, one of the most active trading pools on Solana, with over 140,000 transactions per day

  • POOL_NAME - A human-readable name for display purposes

Add DEX Program IDs

// Common Solana DEX Program IDs
const RAYDIUM_AMM = "675kPX9MHTjS2zt1qfr1NYHuzeLXfQM9H24wFSUt1Mp8";
const RAYDIUM_CLMM = "CAMMCzo5YL8w4VFF8KVHrK22GGUsp5VTaW7grrKgrWqK";
const JUPITER_V6 = "JUP6LkbZbjS1jKKwapdHNy74zcZ3tLUZoi5QNyVTaV4";

What this does:

  • Stores the unique program IDs for major Solana DEX platforms. These are like addresses for the programs (smart contracts) themselves. When someone makes a swap, their transaction calls one of these programs. By checking which program was called, you can identify if the swap came from:

    • Raydium_AMM - Raydium's standard automated market maker

    • Raydium_CLMM - Raydium's concentrated liquidity pools (like Uniswap V3)

    • JUPITER_V6 - Jupiter's DEX aggregator (finds best prices across multiple DEXes)

Add Statistics Tracker

// Statistics
let stats = {
  startTime: Date.now(),
  totalSwaps: 0
};

What this does:

  • It creates a simple object to track two key pieces of information: when the monitor was started (startTime) and the total number of successful swaps detected (totalSwaps).

  • This allows you to view cumulative statistics when you stop the program.

Step 4: Build the Swap Source Identifier

Now you'll add a function to identify which platform initiated each swap.

function identifySwapSource(instructions, accountKeys) {
  for (const instruction of instructions) {
    const programIdx = instruction.programIdIndex;
    const programId = bs58.encode(accountKeys[programIdx]);
    
    if (programId === JUPITER_V6) {
      return "Jupiter";
    } else if (programId === RAYDIUM_CLMM) {
      return "Raydium CLMM";
    } else if (programId === RAYDIUM_AMM) {
      return "Raydium AMM";
    }
  }
  
  return "Other";
}

What this does:

  • This function examines all the instructions in a transaction to figure out where the swap came from.

Understanding how this works:

  • Every Solana transaction contains one or more "instructions" - think of these as individual commands or steps

  • Each instruction calls a specific program (smart contract)

  • The programIdIndex points to which program being called

  • We look up that program's address using accountKeys[programIdx] and convert it from binary to a readable format using bs58.encode()

  • Then we check if it matches any of our known DEX programs (Jupiter, Raydium CLMM, or Raydium AMM)

  • If none match, you label it as "Other" (could be another DEX or aggregator like Orca, Phantom Swap, etc.)

Step 5: Add Display Function

function displaySwap(swapData) {
  stats.totalSwaps++;
  
  console.log("\n" + "=".repeat(80));
  console.log(`✅ SWAP #${stats.totalSwaps} on ${POOL_NAME}`);
  console.log("=".repeat(80));
  
  console.log(`\n🔀 SOURCE:      ${swapData.source}`);
  console.log(`👤 TRADER:      ${swapData.trader}`);
  console.log(`📜 SIGNATURE:   ${swapData.signature}`);
  console.log(`🎰 SLOT:        ${swapData.slot}`);
  console.log(`⏰ TIME:        ${swapData.time}`);
  
  console.log(`\n🔍 EXPLORE:`);
  console.log(`   TX:   https://solscan.io/tx/${swapData.signature}`);
  console.log(`   Pool: https://solscan.io/account/${TARGET_POOL}`);
  
  console.log("\n" + "=".repeat(80) + "\n");
}

What this does:

  • Formats and displays each swap in a readable way to your terminal.

Break down:

  • stats.totalSwaps++ - Increments the counter every time we display a swap

  • "=".repeat(80) - Creates a visual separator line (80 equal signs)

  • SOURCE - Shows which platform was used (Jupiter, Raydium, etc.)

  • TRADER - The Solana wallet address that initiated the swap

  • SIGNATURE - A unique identifier for this transaction (like a transaction hash)

  • SLOT - Solana's equivalent of a block number - tells you exactly when this happened on the blockchain

  • TIME - The local time when you received this swap notification

  • EXPLORE section - Provides clickable links to:

    • View the full transaction details on Solscan (a Solana block explorer)

    • View the pool itself and all its activity

Step 6: Build the Main Monitor Function

Now you'll create the core function that connects to GetBlock and processes blockchain data.

Part A: Start the Function and Connect

async function monitorPoolSwaps() {
  console.log("🚀 Starting Liquidity Pool Swap Monitor");
  console.log(`🎯 Pool: ${POOL_NAME}`);
  console.log(`📍 Address: ${TARGET_POOL}\n`);
  console.log("Waiting for swaps...\n");
  
  return new Promise(async (resolve, reject) => {
    try {
      const client = new Client(ENDPOINT, TOKEN, undefined);
      const stream = await client.subscribe();

What this does:

  • Prints a startup message so you know the monitor is launching

  • Creates a Promise so this function can run asynchronously and handle errors properly

  • new Client(ENDPOINT, TOKEN, undefined)- Creates a connection client using your GetBlock credentials.

  • await client.subscribe() - Opens a bidirectional streaming connection to GetBlock's Yellowstone service. This is the "pipe" through which blockchain data will flow to your application

Part B: Configure Subscription

const request = {
        accounts: {},
        slots: {},
        transactions: {
          pool_swaps: {
            accountInclude: [TARGET_POOL],
            accountExclude: [],
            accountRequired: []
          }
        },
        transactionsStatus: {},
        entry: {},
        blocks: {},
        blocksMeta: {},
        commitment: CommitmentLevel.CONFIRMED,
        accountsDataSlice: [],
        ping: undefined
      };

What this does:

  • Tells GetBlock exactly what data you want to receive

Part C: Handle Incoming Data

stream.on("data", (message) => {
try {
  if (message.pong) {
    stream.write({ ping: { id: message.pong.id } });
    return;
  }
  
  if (message.transaction && message.filters && 
      message.filters.includes('pool_swaps')) {
    
    const tx = message.transaction.transaction;
    const signature = bs58.encode(tx.signature);
    const slot = message.transaction.slot.toString();
    
    const txMessage = tx.transaction.message;
    const accountKeys = txMessage.accountKeys;
    const instructions = txMessage.instructions;

What this does:

  • Sets up an event listener that processes each message GetBlock sends you.

Part D: Process and Display Swaps

// Skip failed transactions
    const txMeta = message.transaction.meta;
    if (txMeta && txMeta.err) return;
    
    // Identify swap source
    const source = identifySwapSource(instructions, accountKeys);
    
    // Get trader (signer)
    const trader = accountKeys.length > 0 
      ? bs58.encode(Buffer.from(accountKeys[0])) 
      : "Unknown";
    
    displaySwap({
      source: source,
      trader: trader,
      signature: signature,
      slot: slot,
      time: new Date().toLocaleTimeString()
    });
  }
} catch (error) {
  console.error(`Error processing transaction: ${error.message}`);
}
});

What this does:

  • processes each transaction and:

    • Checks if the transaction failed - txMeta.err will be present if the transaction failed. We skip these because failed swaps aren't useful to track (they might be due to slippage, insufficient balance, etc.)

    • Identifies the source - Calls our identifySwapSource() function to determine if it came from Jupiter, Raydium, or elsewhere

    • Gets the trader - In Solana, the first account in accountKeys is always the signer (the person who initiated the transaction). We convert it from binary to a readable format

    • Display severything - Calls our displaySwap() function with all the collected information

Part E: Handle Stream Events

stream.on("error", (error) => {
  console.error(`Stream error: ${error.message}`);
  reject(error);
});

stream.on("end", () => resolve());
stream.on("close", () => resolve());

What this does:

  • Handles different connection states such as:

    • error - If something goes wrong with the connection (network issue, GetBlock problem, etc.), we log the error and reject the Promise. This will trigger our auto-reconnect logic later

    • end - The stream ended gracefully (normal shutdown)

    • close - The connection closed (either normally or due to an error)

Both end and close resolve the Promise, which allows the program to shut down cleanly or restart if needed.

Part F: Send Subscription Request

stream.write(request, (err) => {
        if (err) {
          reject(err);
        } else {
          console.log("✅ Subscription active - monitoring pool swaps...\n");
        }
      });
      
    } catch (error) {
      reject(error);
    }
  });
}

What this does:

  • Sends your subscription configuration to GetBlock and confirms the connection is active.

At this stage, your monitor is fully connected, subscribed, and waiting for swaps. Every time someone trades on the SOL/USDC pool through any platform, you'll receive the data within ~400ms and display it.

Step 7: Add Auto-Restart Functionality

Add the code to run your monitor with automatic restart on errors:

async function main() {
  try {
    await monitorPoolSwaps();
  } catch (error) {
    console.error("Monitor crashed:", error.message);
    console.log("Restarting in 5 seconds...");
    setTimeout(main, 5000);
  }
}

What this does:

  • Provides resilience by automatically restarting if something goes wrong.

Step 8: Add Shutdown

Finally, add the code to handle Ctrl+C successfully:

process.on('SIGINT', () => {
  console.log("\n\n🛑 Shutting down...");
  console.log(`Total swaps detected: ${stats.totalSwaps}\n`);
  process.exit(0);
});


main();

Step 9: Test Your Monitor

  1. Run the Monitor

node pool-monitor.js
  1. Expected output:

You'll see:

🚀 Starting Liquidity Pool Swap Monitor
🎯 Pool: SOL/USDC
📍 Address: 8sLbNZoA1cfnvMJLPfp98ZLAnFSYCFApfJKMbiXNLwxj

Waiting for swaps...

✅ Subscription active - monitoring pool swaps...

This means you're connected to GetBlock and monitoring is active!
When a Swap Appears
================================================================================
✅ SWAP #1 on SOL/USDC
================================================================================
🔀 SOURCE:      Jupiter
👤 TRADER:      9wFFyRfZBsuAha4YcuxcXLKwMxJR43S7fPfQLusDBzvT
📜 SIGNATURE:   5TYqzsc8kGY7vMGDZxEbBFWGnEUPTQQMpk9kRvx3H8p2
🎰 SLOT:        376205017
⏰ TIME:        14:32:18
🔍 EXPLORE:
   TX:   https://solscan.io/tx/5TYqzsc8kGY7vMGDZxEbBFWGnEUPTQQMpk9kRvx3H8p2
   Pool: https://solscan.io/account/8sLbNZoA1cfnvMJLPfp98ZLAnFSYCFApfJKMbiXNLwxj
================================================================================

What you see:

  • SOURCE- This swap came through Jupiter's aggregator

  • TRADER - The wallet address that made the swap

  • SIGNATURE - Unique transaction ID (click the link to see full details on Solscan)

  • SLOT - Happened at blockchain slot 376,205,017

  • TIME- Your local time when you received this notification

The SOL/USDC pool typically has 140,000+ swaps per day, so you should see activity within seconds of starting the monitor.

Troubleshooting

  1. Connection Issues:

"Connection refused"
  • Verify your ENDPOINT and TOKEN in pool-monitor.js

  • Check your GetBlock dashboard, confirm the node is active

  • Test your internet connection

  1. No Swaps Showing:

If you don't see any swaps after a minute:

  • The SOL/USDC pool is extremely active, so this is unusual

  • Try visiting the pool on Solscan to verify it's active

  • Check that your subscription was successful (you should see "✅ Subscription active")

  • Try CommitmentLevel.PROCESSED for faster updates

  1. Processing Errors:

Error processing transaction warnings
  • These are normal. Occasionally, some transactions have non-standard formats

  • Your monitor will skip these and continue running

  • Failed transactions are automatically filtered out

Conclusion

In this guide, you've learn how to build a monitor that fetches liquidity pools on Solana DEXes with GetBlock's Yellowstone gRPC. This guide exposes you to the importance of using GetBlock's Yellowstone gRPC and how to set up the project effectively.

Resources:

Last updated

Was this helpful?