How to Track Solana Program Activity: Instructions, Events & CPI Chains

Tools Mentioned in This Article
Compare features, read reviews, and check live health scores on MadeOnSol.

Compare features, read reviews, and check live health scores on MadeOnSol.

Multi-chain RPC provider with free and paid Solana nodes — shared and dedicated endpoints for dApps, bots, and analytics

High-performance Solana RPC node infrastructure for developers and traders

Decentralized RPC network providing fast, resilient Solana node access

Fast and affordable Solana RPC node provider with unlimited requests

Full node RPC access for Solana and 80+ blockchains with simple API keys

Learn how to build a Solana token analytics dashboard that combines real-time price feeds, holder data, trading volume, and liquidity metrics into a single view using TypeScript and the MadeOnSol API.

Solana validators prune old data aggressively, making historical analysis difficult. Learn how to access Solana historical data using BigQuery, Flipside, Dune, Helius DAS API, and archival services — with practical query examples.

Learn how to set up Solana webhooks for real-time blockchain alerts. Compare Helius, QuickNode, Shyft, and Triton webhook providers, with TypeScript code examples for wallet monitoring, NFT sales alerts, and payment confirmation.
Build with MadeOnSol API
KOL tracking, deployer intelligence, DEX streaming, and webhooks. Free API key — 200 requests/day.
import { MadeOnSol } from "madeonsol";
// Get a free API key at madeonsol.com/developer
const client = new MadeOnSol({ apiKey: "msk_your_key" });
const { trades } = await client.kol.feed({ limit: 10 });
const { alerts } = await client.deployer.alerts({ limit: 5 });
const { tools } = await client.tools.search({ q: "trading" });Get weekly Solana ecosystem insights delivered to your inbox.
Most Solana monitoring tutorials stop at token transfers. But the real power of on-chain analysis lies in tracking specific program activity -- understanding what instructions a program executed, which cross-program invocations (CPIs) it triggered, and what custom events it emitted.
Whether you are building a DEX analytics dashboard, monitoring a DeFi protocol for security, or detecting new liquidity pool creation in real-time, you need to go deeper than balance changes. This guide covers the tools and techniques for tracking Solana program activity at every level.
A single Solana transaction can invoke multiple programs through CPI chains. When a user swaps on Jupiter, the transaction might touch Jupiter routing program, Raydium AMM, the SPL Token program, and the Associated Token Account program -- all in one atomic operation.
If you only watch token transfer events, you miss critical context:
To properly track Solana program activity, you need to monitor at the instruction level and understand CPI relationships.
Every Solana transaction contains one or more instructions. Each instruction targets a specific program and includes:
When a program calls another program during execution, that creates an inner instruction (CPI). The runtime records these in a tree structure, so you can trace exactly which program initiated each call.
// Structure of a parsed transaction with inner instructions
interface ParsedTransaction {
transaction: {
message: {
instructions: CompiledInstruction[]; // Top-level instructions
};
};
meta: {
innerInstructions: {
index: number; // Which top-level instruction spawned these
instructions: InnerInstruction[]; // The CPI calls
}[];
logMessages: string[]; // Program log output
};
}
The simplest way to track Solana program activity is through WebSocket log subscriptions. The logsSubscribe RPC method lets you filter for transactions that involve a specific program.
import { Connection, PublicKey } from "@solana/web3.js";
const connection = new Connection("https://mainnet.helius-rpc.com/?api-key=YOUR_KEY");
const RAYDIUM_AMM = "675kPX9MHTjS2zt1qfr1NYHuzeLXfQM9H24wFSUt1Mp8";
const subscriptionId = connection.onLogs(
new PublicKey(RAYDIUM_AMM),
(logs, context) => {
if (logs.err) return; // Skip failed transactions
console.log("Signature:", logs.signature);
console.log("Slot:", context.slot);
// Parse log messages for program-specific events
for (const log of logs.logs) {
if (log.includes("ray_log:")) {
// Raydium emits base64-encoded event data after "ray_log:"
const eventData = log.split("ray_log: ")[1];
const decoded = Buffer.from(eventData, "base64");
console.log("Raydium event data:", decoded);
}
}
},
"confirmed"
);
This approach works well for moderate-volume programs but has limitations. Public RPC WebSocket connections are unreliable under load, and you only get log messages -- not the full parsed instruction data. For production workloads, consider gRPC streaming with a provider like Triton or Helius.
Raw instruction data is just bytes. To understand what a program actually did, you need to decode those bytes using the program Interface Definition Language (IDL). Most Solana programs built with the Anchor framework publish their IDL on-chain.
import { Program, Idl } from "@coral-xyz/anchor";
import { Connection, PublicKey } from "@solana/web3.js";
const connection = new Connection("https://mainnet.helius-rpc.com/?api-key=YOUR_KEY");
// Fetch the IDL for an Anchor program
const programId = new PublicKey("PROGRAM_ID_HERE");
const idl = await Program.fetchIdl(programId, { connection });
if (!idl) {
console.log("No IDL found -- program may not be Anchor-based");
// Fall back to manual decoding (see next section)
}
// Create a Program instance for decoding
const program = new Program(idl, { connection });
// Decode an instruction from a transaction
function decodeInstruction(ix: { data: Buffer; programId: PublicKey }) {
try {
const decoded = program.coder.instruction.decode(ix.data);
if (decoded) {
console.log("Instruction name:", decoded.name);
console.log("Arguments:", JSON.stringify(decoded.data, null, 2));
}
return decoded;
} catch (err) {
console.log("Failed to decode -- instruction may use a different format");
return null;
}
}
For programs that do not use Anchor, you need to decode instructions manually based on the program documentation or source code. The first 8 bytes of instruction data typically contain a discriminator (a hash of the instruction name), followed by serialized arguments.
Anchor programs can emit structured events via the emit! macro. These events appear in transaction logs with a specific prefix that makes them identifiable and decodable.
import { BorshCoder, EventParser } from "@coral-xyz/anchor";
// Create an event parser from the program IDL
const coder = new BorshCoder(idl);
const eventParser = new EventParser(programId, coder);
// Parse events from transaction log messages
function parseEventsFromLogs(logs: string[]) {
const events = [];
const generator = eventParser.parseLogs(logs);
for (const event of generator) {
events.push({
name: event.name,
data: event.data,
});
}
return events;
}
const tx = await connection.getTransaction(signature, {
maxSupportedTransactionVersion: 0,
});
if (tx?.meta?.logMessages) {
const events = parseEventsFromLogs(tx.meta.logMessages);
for (const event of events) {
console.log("Event:", event.name);
console.log("Data:", event.data);
}
}
Non-Anchor programs often use a simpler pattern: they write structured data to logs using msg! or sol_log_data. You can identify these by their program-specific prefixes (like the Raydium ray_log: prefix shown earlier).
A single user action can trigger a cascade of cross-program invocations. Tracking the full CPI chain tells you the complete story of what happened in a transaction.
async function inspectCPIs(signature: string) {
const tx = await connection.getTransaction(signature, {
maxSupportedTransactionVersion: 0,
});
if (!tx?.meta) return;
const accountKeys = tx.transaction.message.getAccountKeys();
for (let i = 0; i < tx.transaction.message.compiledInstructions.length; i++) {
const ix = tx.transaction.message.compiledInstructions[i];
const programId = accountKeys.get(ix.programIdIndex)?.toBase58();
console.log("Top-level program:", programId);
// Find inner instructions (CPIs) spawned by this instruction
const innerGroup = tx.meta.innerInstructions?.find((g) => g.index === i);
if (innerGroup) {
for (const inner of innerGroup.instructions) {
const innerProgram = accountKeys.get(inner.programIdIndex)?.toBase58();
console.log(" -> CPI:", innerProgram);
}
}
}
}
For deeper analysis of transaction structures, check out How to Read Solana Transactions.
One practical application of program activity tracking is detecting new liquidity pools the moment they are created. Pool creation events often precede significant price action, making real-time detection valuable for traders and analytics platforms.
const RAYDIUM_AMM = new PublicKey("675kPX9MHTjS2zt1qfr1NYHuzeLXfQM9H24wFSUt1Mp8");
const INITIALIZE2_DISCRIMINATOR = Buffer.from([175, 175, 109, 31, 13, 152, 155, 237]);
connection.onLogs(RAYDIUM_AMM, async (logInfo) => {
if (logInfo.err) return;
const tx = await connection.getTransaction(logInfo.signature, {
maxSupportedTransactionVersion: 0,
});
if (!tx) return;
const accountKeys = tx.transaction.message.getAccountKeys();
for (const ix of tx.transaction.message.compiledInstructions) {
const programId = accountKeys.get(ix.programIdIndex)?.toBase58();
if (programId !== RAYDIUM_AMM.toBase58()) continue;
const data = Buffer.from(ix.data);
if (data.subarray(0, 8).equals(INITIALIZE2_DISCRIMINATOR)) {
console.log("New Raydium pool detected!", logInfo.signature);
// Extract pool accounts from ix.accountKeyIndexes
}
}
}, "confirmed");
Tracking program activity is essential for monitoring DeFi protocol health. You can watch for specific instructions like liquidations, large deposits, or governance votes.
// Monitor a lending protocol for liquidation events
const LENDING_PROGRAM = new PublicKey("LENDING_PROGRAM_ID");
connection.onLogs(LENDING_PROGRAM, (logInfo) => {
if (logInfo.err) return;
for (const log of logInfo.logs) {
// Anchor programs log events with a specific format
if (log.includes("LiquidateObligationEvent")) {
console.log("Liquidation detected:", logInfo.signature);
// Fetch full tx for details: borrower, collateral, debt amount
}
if (log.includes("DepositReserveLiquidity")) {
// Track large deposits for whale monitoring
console.log("Deposit event:", logInfo.signature);
}
}
}, "confirmed");
For production monitoring at scale, log subscriptions over WebSocket will not keep up with high-throughput programs. Use gRPC streaming to get transaction-level filtering with backpressure handling and guaranteed delivery.
Several tools in the Solana ecosystem make it easier to inspect and track program activity without writing custom code:
Helius provides enhanced transaction APIs that return pre-parsed instruction data, account changes, and token transfers in a single call. Their webhook system can notify you when specific programs are invoked, removing the need to maintain your own WebSocket connections.
SolanaFM offers an explorer with deep instruction-level decoding. It automatically resolves IDLs for known programs and displays decoded instruction arguments alongside the raw data. Useful for manual investigation and understanding unfamiliar programs.
Solscan provides a program analytics dashboard showing instruction frequency, unique callers, and activity trends over time. Their API includes endpoints for fetching all transactions involving a specific program with pagination support.
Triton operates Yellowstone gRPC infrastructure that supports program-level transaction filtering at the validator level. This is the most performant option for real-time program monitoring, as data is filtered before it reaches your application.
When tracking high-activity programs (DEX routers, token programs), you will encounter significant data volumes. A few strategies to keep your monitoring system stable:
getMultipleTransactions or batch JSON-RPC requestsmaxSupportedTransactionVersion: 0 when fetching transactions -- legacy calls will miss transactions that use address lookup tablesYou do not need a validator or full node. RPC providers like Helius and Triton offer WebSocket log subscriptions and gRPC streaming that let you monitor any program in real-time. For lower-volume use cases, the logsSubscribe RPC method on any reliable endpoint is sufficient. For high-throughput programs, gRPC streaming with Yellowstone filters gives you validator-level performance without operating infrastructure.
Program logs are text messages emitted by programs during execution using the msg! or emit! macros. They are human-readable (or base64-encoded structured data) and useful for detecting events. Inner instructions are the actual CPI calls a program makes to other programs -- they contain the full instruction data, accounts, and program ID. Logs tell you what happened; inner instructions tell you how programs interacted with each other.
Yes, but it requires more work. Non-Anchor programs use custom serialization formats for their instruction data. You need the program source code or documentation to understand the byte layout. Some programs publish their own IDL-like specifications. Tools like SolanaFM maintain a database of known instruction decoders for popular programs, which can save you from writing custom parsing logic.
For monitoring several programs simultaneously, gRPC streaming is the most efficient approach. Yellowstone gRPC supports multiple program filters in a single subscription, so you can watch DEX programs, lending protocols, and token programs in one connection. Alternatively, Helius webhooks let you configure multiple program-based triggers that all push to a single endpoint. Avoid opening separate WebSocket connections per program -- that approach does not scale past a handful of programs.
This guide covers techniques current as of April 2026. Program interfaces and IDL formats may change as the Solana ecosystem evolves. Always refer to official program documentation for the latest instruction layouts.