Building Pump.fun to PumpSwap and Raydium Migrations Listener with GetBlock
A step-by-step guide on how to build a real-time listener for Pump.fun token migrations to both PumpSwap and Raydium using GetBlock API
Pump.fun is a Solana-based memecoin launchpad that uses a bonding curve mechanism to initiate token distribution. When a token reaches an approximate market cap of $69,000, it graduates from the bonding curve. At this point, $12,000 of liquidity is deposited to either Raydium DEX or PumpSwap (Pump.fun's native DEX). This migration creates immediate trading opportunities for developers and traders who can detect these events in real-time.
How Pump.fun Migrations Work
Tokens on Pump.fun start trading on a bonding curve mechanism. When the bonding curve reaches completion (approximately $69,000 market cap), the protocol executes a migration.
Migration Flow:
Bonding Curve Phase: Token trades on Pump.fun using bonding curve pricing
Completion Trigger: Bonding curve reaches ~$69k market cap
Migration Transaction: Pump.fun migration account executes the migration
Liquidity Deployment: $12,000 of liquidity is deposited into either:
PumpSwap (Pump.fun's DEX) - Most common since March 2025
Raydium AMM - Less common, but still happens

Key Addresses
Pump.fun Program
6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P
Pump.fun Migration Account
39azUYFWPz3VHgKCf3VChUwbpURdCHRxjWVowf5jUJjg
Raydium Liquidity Pool V4
675kPX9MHTjS2zt1qfr1NYHuzeLXfQM9H24wFSUt1Mp8
In this guide, you will learn how to:
Get a GetBlock Access Token for Solana's API endpoint
Build a WebSocket listener that detects Pump.fun migrations to both PumpSwap and Raydium
Process transaction logs to extract token and pool information
Distinguish between PumpSwap and Raydium migrations
Handle reconnections and error scenarios
Display migration data in a user-friendly format
Prerequisites
Basic understanding of JavaScript
A GetBlock account
Node.js installed (v18 or higher)
Technology Stack
Node.js: JavaScript runtime environment for building server applications
@solana/web3.js: Official Solana JavaScript SDK for blockchain interactions
ws: WebSocket client library for real-time connections
dotenv: Environment variable management for secure configuration
Step 1: Project Initialization
Set up your project directory using this command:
# Create project folder
mkdir pumpfun-migrations-listener
cd pumpfun-migrations-listener
# Initialize npm
npm init -yInstall Dependencies:
npm install @solana/web3.js ws dotenvConfigure
package.json:
{
"name": "pumpfun-migrations-listener",
"version": "1.0.0",
"type": "module",
"description": "Real-time listener for Pump.fun migrations to PumpSwap and Raydium",
"main": "server.js",
"scripts": {
"start": "node server.js"
},
"keywords": ["pump.fun", "pumpswap", "raydium", "solana", "getblock"],
"author": "GetBlock",
"license": "MIT",
"dependencies": {
"@solana/web3.js": "^1.98.4",
"dotenv": "^17.2.3",
"ws": "^8.18.3"
}
}"type": "module"- Enables ES6 import/export syntax (required for modern JavaScript)"main": "server.js"- Specifies entry point of your application"scripts"- Defines shortcuts likenpm start
Get GetBlock's API Access Token
Log in to your GetBlock account
On your dashboard, scroll and click on Get Endpoint
Select the Solana Mainnet network
Under API Interface, select WebSocket (wss://)
Click on Create to get your endpoint
Your WebSocket endpoint will look like:
wss://go.getblock.us/<ACCESS_TOKEN>Also get the HTTP endpoint for transaction fetching:
Return to the dashboard
Select JSON-RPC (https://) under API Interface
Click Create
Your HTTP endpoint will look like:
https://go.getblock.us/<ACCESS_TOKEN>Save both endpoints in a
.envfile in your project root:
GETBLOCK_WS_ENDPOINT=wss://go.getblock.us/<ACCESS_TOKEN>
GETBLOCK_HTTP_ENDPOINT=https://go.getblock.us/<ACCESS_TOKEN>Keep your endpoints safe, as they contain your access token
Project Structure
Create the following files to have a basic structure for your project:
pumpfun-migrations-listener/
├── server.js # Main application logic
├── .env # Environment variables (add to .gitignore)
├── .gitignore # Git ignore file
└── package.json # Project configurationCreate a
.gitignorefile:
node_modules/
.env
*.logStep 2: Server File Setup
Your script will be written inside server.js
1. Import Dependencies and Configuration
import { Connection, PublicKey } from '@solana/web3.js';
import WebSocket from 'ws';
import dotenv from 'dotenv';
// Load environment variables
dotenv.config();
// Configuration
const WS_ENDPOINT = process.env.GETBLOCK_WS_ENDPOINT;
const HTTP_ENDPOINT = process.env.GETBLOCK_HTTP_ENDPOINT;
const MIGRATION_ACCOUNT = '39azUYFWPz3VHgKCf3VChUwbpURdCHRxjWVowf5jUJjg';
const RAYDIUM_PROGRAM = '675kPX9MHTjS2zt1qfr1NYHuzeLXfQM9H24wFSUt1Mp8';
const PUMPFUN_PROGRAM = '6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P';
// Validate configuration
if (!WS_ENDPOINT || !HTTP_ENDPOINT) {
console.error('❌ Missing required environment variables');
console.error('Please set GETBLOCK_WS_ENDPOINT and GETBLOCK_HTTP_ENDPOINT in .env file');
process.exit(1);
}
console.log('✅ Configuration loaded successfully');What this does:
Imports necessary libraries for Solana interactions and WebSocket connections
Loads API endpoints securely from environment variables
Validates that the required configuration is present
Defines the key addresses we'll monitor for migrations
MIGRATION_ACCOUNT- This is the Pump.fun account that executes all migrationsRAYDIUM_PROGRAM- The Raydium program ID to detect Raydium migrations.PUMPFUN_PROGRAM- The Pump.fun program ID to detect PumpSwap migrations.
2. Create the Migration Listener Class
class PumpFunMigrationListener {
constructor() {
this.ws = null;
this.connection = new Connection(HTTP_ENDPOINT, 'confirmed');
this.subscriptionId = null;
this.isConnected = false;
this.reconnectAttempts = 0;
this.maxReconnectAttempts = 10;
this.raydiumMigrationCount = 0;
this.pumpswapMigrationCount = 0;
this.migrations = [];
this.startTime = Date.now();
this.totalLogsReceived = 0;
}Breaking down the constructor:
The constructor initializes all the state we need to track. Let me explain each property:
this.ws- Will hold our WebSocket connection to GetBlockthis.connection- HTTP connection for fetching full transaction detailsthis.subscriptionId- The ID returned when we subscribe to logsthis.isConnected- Boolean flag to track connection statusthis.reconnectAttempts- Counter for reconnection attempts (we limit it to 10)this.raydiumMigrationCount&this.pumpswapMigrationCount- Separate counters for each migration typethis.migrations- Array to store all detected migrationsthis.startTime- Timestamp when listener started (for runtime tracking)this.totalLogsReceived- Counter for all logs received (helps with debugging)
3. Create an Interval check
start() {
console.log('🚀 Starting Pump.fun Migration Listener...');
console.log('📡 Monitoring account:', MIGRATION_ACCOUNT);
console.log('🎯 Tracking: Raydium + PumpSwap migrations');
console.log('');
this.connect();
this.checkRunning();
}
checkRunning() {
setInterval(() => {
const runtime = Math.floor((Date.now() - this.startTime) / 1000);
const hours = Math.floor(runtime / 3600);
const minutes = Math.floor((runtime % 3600) / 60);
const seconds = runtime % 60;
console.log(`💓 Alive | Runtime: ${hours}h ${minutes}m ${seconds}s | Logs: ${this.totalLogsReceived} | Raydium: ${this.raydiumMigrationCount} | PumpSwap: ${this.pumpswapMigrationCount}`);
}, 60000);
}What this does:
The
start()method kicks everything off. It checks if the Websocket is connected or not.The
checkRunning()method creates a timer that runs every 60 seconds (60000 milliseconds). This serves two purposes:It shows the listener is still running
It provides statistics: runtime, total logs received, and migrations detected by type
Migration isn't frequent and may not occur within an hour. This is good for debugging—if no logs appear, it's a sign there's an issue with the subscription. This ensures that monitoring is active.
4. Create GetBlock Websocket Connection
connect() {
console.log('🔌 Connecting to GetBlock WebSocket...');
this.ws = new WebSocket(WS_ENDPOINT);
this.ws.on('open', () => this.handleOpen());
this.ws.on('message', (data) => this.handleMessage(data));
this.ws.on('error', (error) => this.handleError(error));
this.ws.on('close', () => this.handleClose());
}
handleOpen() {
console.log('✅ Connected to GetBlock WebSocket');
this.isConnected = true;
this.reconnectAttempts = 0;
this.subscribeToLogs();
}What this does:
The
connect()method creates a new WebSocket connection to GetBlock. Then it sets up four event listeners:open- Called when the connection is establishedmessage- Called whenever we receive dataerror- Called if something goes wrongclose- Called when the connection drops
When the connection opens,
handleOpen()runs. It setsisConnectedto true, resets the reconnection counter (since you've successfully connected), and most importantly, subscribes to the logs you need.
5. Subscribe to Migration Events
subscribeToLogs() {
const subscribeRequest = {
jsonrpc: '2.0',
id: 1,
method: 'logsSubscribe',
params: [
{
mentions: [MIGRATION_ACCOUNT]
},
{
commitment: 'confirmed'
}
]
};
console.log('📡 Subscribing to migration events...');
this.ws.send(JSON.stringify(subscribeRequest));
}What this does:
Uses Solana's
logsSubscribemethod to monitor transactionsFilters for transactions mentioning the Pump.fun migration account
Uses
confirmedcommitment for faster notifications (1-2 seconds)
6. Handle Incoming Messages
async handleMessage(data) {
try {
const message = JSON.parse(data.toString());
if (message.result && !message.params) {
this.subscriptionId = message.result;
console.log(`✅ Subscribed with ID: ${this.subscriptionId}`);
console.log('⏳ Listening for migrations...\n');
return;
}
if (message.params && message.params.result) {
await this.processLog(message.params.result);
}
} catch (error) {
console.error('❌ Error handling message:', error.message);
}
}What this does:
Parses incoming WebSocket messages
Handles subscription confirmation and stores subscription ID
Processes log notifications for migration detection
7. Process Transaction Logs
async processLog(result) {
const { value } = result;
this.totalLogsReceived++;
console.log(`📨 Log #${this.totalLogsReceived} - TX: ${value.signature.substring(0, 20)}...`);
if (value.err) {
console.log(' ↳ Skipped (failed transaction)');
return;
}
const hasMigration = value.logs.some(
(log) =>
log.includes("Instruction: Migrate") ||
log.includes("migrate") ||
log.includes("initialize2")
);
if (hasMigration) {
console.log(" ↳ 🎯 MIGRATION DETECTED!");
await this.fetchAndProcessTransaction(value.signature);
} else {
console.log(" ↳ Not a migration");
}
}What this does:
Checks transaction logs for migration keywords like:

Skips failed transactions
Fetches full transaction details when migration is detected
8. Fetch Full Transaction Details
async fetchAndProcessTransaction(signature) {
try {
await new Promise((resolve) => setTimeout(resolve, 1000));
const tx = await this.connection.getTransaction(signature, {
maxSupportedTransactionVersion: 0,
commitment: 'confirmed'
});
if (!tx) {
console.log('⚠️ Transaction not found, skipping...');
return;
}
const migrationData = this.extractMigrationData(tx, signature);
if (migrationData) {
this.migrations.push(migrationData);
if (migrationData.type === 'raydium') {
this.raydiumMigrationCount++;
} else if (migrationData.type === 'pumpswap') {
this.pumpswapMigrationCount++;
}
this.displayMigration(migrationData);
}
} catch (error) {
console.error('❌ Error fetching transaction:', error.message);
}
}What this does:
Waits 1 second for the transaction to propagate
Fetches complete transaction from Solana via HTTP
Extracts and displays migration data
9. Extract Migration Data
extractMigrationData(tx, signature) {
try {
let accountKeys, instructions;
if (tx.transaction.message.addressTableLookups) {
accountKeys = tx.transaction.message.staticAccountKeys ||
tx.transaction.message.accountKeys;
instructions = tx.transaction.message.compiledInstructions ||
tx.transaction.message.instructions;
} else {
accountKeys = tx.transaction.message.accountKeys;
instructions = tx.transaction.message.instructions;
}
if (!Array.isArray(instructions)) {
instructions = Object.values(instructions);
}
for (const instruction of instructions) {
let programId;
if (instruction.programIdIndex !== undefined) {
programId = accountKeys[instruction.programIdIndex];
} else if (instruction.programId) {
programId = instruction.programId;
}
// Check for Raydium migration
if (programId && programId.toString() === RAYDIUM_PROGRAM) {
const accounts = instruction.accounts;
if (!accounts || accounts.length < 7) continue;
return {
type: 'raydium',
signature: signature,
slot: tx.slot,
blockTime: tx.blockTime,
poolAddress: accountKeys[accounts[1]].toString(),
tokenAddress: accountKeys[accounts[5]].toString(),
quoteMint: accountKeys[accounts[6]].toString(),
lpMint: accountKeys[accounts[4]].toString(),
};
}
// Check for PumpSwap migration
if (programId && programId.toString() === PUMPFUN_PROGRAM) {
return {
type: 'pumpswap',
signature: signature,
slot: tx.slot,
blockTime: tx.blockTime,
tokenAddress: accountKeys[2] ? accountKeys[2].toString() : 'Unknown',
bondingCurve: accountKeys[1] ? accountKeys[1].toString() : 'Unknown',
destination: 'PumpSwap',
};
}
}
} catch (error) {
console.error('❌ Error extracting migration data:', error.message);
}
return null;
}What this does:
Handles both versioned and legacy Solana transactions
Detects Raydium migrations by checking for Raydium program ID
Detects PumpSwap migrations by checking for Pump.fun program ID
Extracts relevant addresses (token, pool, LP mint)
10. Display Migration Results
displayMigration(data) {
if (data.type === 'raydium') {
console.log('\n🚀 ═══════════════════════════════════════════════════');
console.log(' NEW PUMP.FUN → RAYDIUM MIGRATION DETECTED!');
console.log('═══════════════════════════════════════════════════\n');
console.log(`Migration #${this.raydiumMigrationCount} (Raydium)\n`);
console.log(`📊 Token Address: ${data.tokenAddress}`);
console.log(`🏊 Pool Address: ${data.poolAddress}`);
console.log(`💧 LP Mint: ${data.lpMint}`);
console.log(`💰 Quote Token: ${data.quoteMint}\n`);
console.log(`📝 Transaction: ${data.signature}`);
console.log(`🔢 Slot: ${data.slot}`);
if (data.blockTime) {
console.log(`⏰ Time: ${new Date(data.blockTime * 1000).toISOString()}`);
}
console.log(`\n🔗 View on Solscan:`);
console.log(` https://solscan.io/tx/${data.signature}\n`);
console.log(`🔗 Trade on Raydium:`);
console.log(` https://raydium.io/swap/?inputMint=sol&outputMint=${data.tokenAddress}\n`);
console.log('═══════════════════════════════════════════════════\n');
} else if (data.type === 'pumpswap') {
console.log('\n💧 ═══════════════════════════════════════════════════');
console.log(' NEW PUMP.FUN → PUMPSWAP MIGRATION DETECTED!');
console.log('═══════════════════════════════════════════════════\n');
console.log(`Migration #${this.pumpswapMigrationCount} (PumpSwap)\n`);
console.log(`📊 Token Address: ${data.tokenAddress}`);
console.log(`🎯 Bonding Curve: ${data.bondingCurve}`);
console.log(`🏪 Destination: ${data.destination}\n`);
console.log(`📝 Transaction: ${data.signature}`);
console.log(`🔢 Slot: ${data.slot}`);
if (data.blockTime) {
console.log(`⏰ Time: ${new Date(data.blockTime * 1000).toISOString()}`);
}
console.log(`\n🔗 View on Solscan:`);
console.log(` https://solscan.io/tx/${data.signature}\n`);
console.log(`🔗 View Token:`);
console.log(` https://pump.fun/${data.tokenAddress}\n`);
console.log('═══════════════════════════════════════════════════\n');
}
}What this does:
Displays Raydium migrations with pool and LP mint info
Displays PumpSwap migrations with bonding curve info
Provides links to Solscan and trading platforms
11. Handle Reconnections
handleError(error) {
console.error('❌ WebSocket error:', error.message);
}
handleClose() {
this.isConnected = false;
console.log('🔌 Connection closed');
if (this.reconnectAttempts < this.maxReconnectAttempts) {
this.reconnectAttempts++;
const delay = Math.min(1000 * Math.pow(2, this.reconnectAttempts), 30000);
console.log(`🔄 Reconnecting in ${delay / 1000}s (attempt ${this.reconnectAttempts}/${this.maxReconnectAttempts})...`);
setTimeout(() => {
this.connect();
}, delay);
} else {
console.error('❌ Max reconnection attempts reached. Exiting...');
process.exit(1);
}
}
stop() {
console.log('👋 Stopping listener...');
if (this.ws) {
this.ws.close();
}
}
}What this does:
Implements exponential backoff for reconnections (2s, 4s, 8s, etc.)
Caps reconnection delay at 30 seconds
Exits after 10 failed reconnection attempts
12. Initialize and Run
const listener = new PumpFunMigrationListener();
listener.start();
process.on('SIGINT', () => {
console.log('\n👋 Shutting down...');
listener.stop();
process.exit(0);
});
process.on('SIGTERM', () => {
listener.stop();
process.exit(0);
});
process.on('uncaughtException', (error) => {
console.error('💥 Uncaught exception:', error);
listener.stop();
process.exit(1);
});
process.on('unhandledRejection', (error) => {
console.error('💥 Unhandled rejection:', error);
listener.stop();
process.exit(1);
});What this does:
Handles graceful shutdown on Ctrl+C
Catches uncaught errors
Step 4: Testing Your Listener
Start your migration listener:
npm startExpected Output:
✅ Configuration loaded successfully
🚀 Starting Pump.fun Migration Listener...
📡 Monitoring account: 39azUYFWPz3VHgKCf3VChUwbpURdCHRxjWVowf5jUJjg
🎯 Tracking: Raydium + PumpSwap migrations
🔌 Connecting to GetBlock WebSocket...
✅ Connected to GetBlock WebSocket
📡 Subscribing to migration events...
✅ Subscribed with ID: 17473
⏳ Listening for migrations...Test 1: Real-time PumpSwap Migration
When a token migrates to PumpSwap:
📨 Log #4 - TX: 2QAbbdjj8P5qk3gzoey5...
📊 Token Address: 3MSQF3mHFL9CuzKtUg6zp3GDdmGgDpPs7tQ1VPoXabTq
🎯 Bonding Curve: 39azUYFWPz3VHgKCf3VChUwbpURdCHRxjWVowf5jUJjg
🏪 Destination: PumpSwap
📝 Transaction: G6YuhMQtC5vonGFDwGDUsy5Uukb7JvkzQeB91FDfbRCYL8fNzvig4so4ZXVnqWNsNZWjLWf2rs5xf55KDZ7wXTW
🔢 Slot: 382201040
⏰ Time: 2025-11-24T12:30:13.000Z
🔗 View on Solscan:
https://solscan.io/tx/G6YuhMQtC5vonGFDwGDUsy5Uukb7JvkzQeB91FDfbRCYL8fNzvig4so4ZXVnqWNsNZWjLWf2rs5xf55KDZ7wXTW
🔗 View Token:
https://pump.fun/3MSQF3mHFL9CuzKtUg6zp3GDdmGgDpPs7tQ1VPoXabTqTest 2: Real-time Raydium Migration
When a token migrates to Raydium (rare):
📨 Log #7 - TX: 5yK8TqPAL2kNXj9bVZmF...
↳ 🎯 MIGRATION DETECTED!
🚀 ═══════════════════════════════════════════════════
NEW PUMP.FUN → RAYDIUM MIGRATION DETECTED!
═══════════════════════════════════════════════════
Migration #1 (Raydium)
📊 Token Address: 8sLbNZoA1cfnvMJLPfp98ZLAnFSYCFApfJKMbiXNLwxj
...Test 3: Check if alive
Every minute:
💓 Alive | Runtime: 0h 15m 30s | Logs: 23 | Raydium: 1 | PumpSwap: 18Troubleshooting
1. Missing access token or Connection error
//Missing api key or environment variable
❌ Missing required environment variables
Please set GETBLOCK_WS_ENDPOINT and GETBLOCK_HTTP_ENDPOINT in .env file
//Connection error(403)
🔌 Connecting to GetBlock WebSocket...
❌ WebSocket error: Unexpected server response: 403
🔌 Connection closedThis means that:
You haven't set up your environment variable as follows in this guide
Your Access token is incorrect or incomplete
2. Only seeing PumpSwap migrations or no migration at all
This is expected. Since March 2025, most tokens (95%+) have migrated to PumpSwap instead of Raydium. Also, migration doesn't happen frequently.
Not receiving any logs
Solution:
Verify subscription succeeded (you should see
✅ Subscribed with ID)Check GetBlock dashboard for API usage limits
Verify transactions exist at Solscan
Conclusion
In this guide, you've built an app that listens to Pump.fun token migrations to both PumpSwap and Raydium using GetBlock's Solana infrastructure. You learn:
How to connect to GetBlock's WebSocket API for real-time blockchain data
How to subscribe to specific account activities on Solana
How to distinguish between PumpSwap and Raydium migrations
How to extract token addresses and pool information from transactions
How to handle versioned Solana transactions
How to implement robust reconnection logic
Additional Resources
Last updated
Was this helpful?