Overview
Account monitoring lets you watch Solana accounts as they change. Track balances, data updates, ownership changes, and when accounts are created or deleted - all in real-time.
You’ll need: Run npm install @triton-one/yellowstone-grpc
first
Installation
npm install @triton-one/yellowstone-grpc
Complete Working Example
Here’s a ready-to-use monitor that tracks account changes:
const Client = require ( "@triton-one/yellowstone-grpc" ). default ;
const { CommitmentLevel } = require ( "@triton-one/yellowstone-grpc" );
// Example: Monitor Token Program accounts
const TOKEN_PROGRAM = "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" ;
// Connect to Yellowstone gRPC
const getClient = async () => {
let client = false ;
try {
client = new Client (
process . env . GRPC_ENDPOINT || "https://grpc.solanatracker.io" ,
process . env . GRPC_API_KEY ,
{
"grpc.max_receive_message_length" : 100 * 1024 * 1024 ,
}
);
const version = await client . getVersion ();
if ( version ) {
console . log ( "Connected to Yellowstone gRPC! Version:" , version );
return client ;
}
} catch ( e ) {
console . error ( "Failed to connect:" , e );
}
if ( ! client ) {
throw new Error ( "Failed to connect!" );
}
};
// Track account updates
const accountStats = {
totalUpdates: 0 ,
uniqueAccounts: new Set (),
totalLamports: 0 ,
lastReportTime: Date . now ()
};
( async () => {
const client = await getClient ();
const stream = await client . subscribe ();
// Handle stream lifecycle
const streamClosed = new Promise (( resolve , reject ) => {
stream . on ( "error" , ( error ) => {
console . error ( "Stream error:" , error );
});
stream . on ( "end" , () => {
console . log ( "Stream ended" );
resolve ();
});
stream . on ( "close" , () => {
console . log ( "Stream closed" );
resolve ();
});
});
// Handle incoming account updates
stream . on ( "data" , ( data ) => {
if ( data ?. account ) {
const account = data . account . account ;
accountStats . totalUpdates ++ ;
accountStats . uniqueAccounts . add ( account . pubkey );
console . log ( ` \n [Account Update # ${ accountStats . totalUpdates } ]` );
console . log ( ` Account: ${ account . pubkey . slice ( 0 , 12 ) } ...` );
console . log ( ` Owner: ${ account . owner . slice ( 0 , 12 ) } ...` );
console . log ( ` Lamports: ${ account . lamports . toLocaleString () } ` );
console . log ( ` Data Size: ${ account . data ?. length || 0 } bytes` );
console . log ( ` Slot: ${ data . account . slot } ` );
console . log ( ` Executable: ${ account . executable ? 'Yes' : 'No' } ` );
console . log ( ` Rent Epoch: ${ account . rentEpoch } ` );
accountStats . totalLamports += account . lamports ;
// Report summary every 100 updates
if ( accountStats . totalUpdates % 100 === 0 ) {
const now = Date . now ();
const elapsed = ( now - accountStats . lastReportTime ) / 1000 ;
const updatesPerSec = 100 / elapsed ;
const avgLamports = accountStats . totalLamports / accountStats . totalUpdates ;
console . log ( ` \n === Account Summary ===` );
console . log ( ` Total Updates: ${ accountStats . totalUpdates } ` );
console . log ( ` Unique Accounts: ${ accountStats . uniqueAccounts . size } ` );
console . log ( ` Updates/sec: ${ updatesPerSec . toFixed ( 1 ) } ` );
console . log ( ` Avg Lamports: ${ avgLamports . toLocaleString () } ` );
console . log ( ` Updates/Account: ${ ( accountStats . totalUpdates / accountStats . uniqueAccounts . size ). toFixed ( 2 ) } ` );
accountStats . lastReportTime = now ;
}
}
});
// Subscribe to accounts
// Customize this request to watch the accounts you want
const request = {
accounts: {
tokenAccounts: {
account: [], // Specific accounts (empty = all)
owner: [ TOKEN_PROGRAM ], // Filter by owner program
filters: [
{ dataSize: 165 } // Optional: filter by data size
]
}
},
slots: {},
transactions: {},
transactionsStatus: {},
entry: {},
blocks: {},
blocksMeta: {},
accountsDataSlice: [], // Optional: slice data to reduce bandwidth
ping: undefined ,
commitment: CommitmentLevel . CONFIRMED ,
};
// Send subscribe request
await new Promise (( resolve , reject ) => {
stream . write ( request , ( err ) => {
if ( err === null || err === undefined ) {
console . log ( "✅ Monitoring accounts \n " );
resolve ();
} else {
reject ( err );
}
});
}). catch (( reason ) => {
console . error ( "Subscribe failed:" , reason );
throw reason ;
});
await streamClosed ;
})();
// Graceful shutdown
process . on ( 'SIGINT' , () => {
console . log ( ' \n\n Shutting down gracefully...' );
console . log ( `Final Stats - Updates: ${ accountStats . totalUpdates } , Accounts: ${ accountStats . uniqueAccounts . size } ` );
process . exit ( 0 );
});
How to Filter Accounts
Specific Accounts
By Program Owner
Advanced Filters
Watch specific accounts by their address: const request = {
accounts: {
specificAccounts: {
account: [
"EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v" , // USDC mint
"So11111111111111111111111111111111111111112" // Wrapped SOL
],
owner: [],
filters: []
}
},
commitment: CommitmentLevel . CONFIRMED ,
// ... other fields
};
More Examples
Example 2: Watch a Specific Wallet
Track a single wallet and see all its changes:
const WALLET_ADDRESS = "YourWalletAddressHere" ;
const walletStats = {
updateCount: 0 ,
lamportHistory: [],
lastBalance: 0
};
( async () => {
const client = await getClient ();
const stream = await client . subscribe ();
const streamClosed = new Promise (( resolve , reject ) => {
stream . on ( "error" , ( error ) => console . error ( "Stream error:" , error ));
stream . on ( "end" , () => resolve ());
stream . on ( "close" , () => resolve ());
});
stream . on ( "data" , ( data ) => {
if ( data ?. account ) {
const account = data . account . account ;
walletStats . updateCount ++ ;
const balanceChange = account . lamports - walletStats . lastBalance ;
walletStats . lamportHistory . push ( account . lamports );
if ( walletStats . lamportHistory . length > 100 ) {
walletStats . lamportHistory . shift ();
}
walletStats . lastBalance = account . lamports ;
}
});
const request = {
accounts: {
wallet: {
account: [ WALLET_ADDRESS ],
owner: [],
filters: []
}
},
slots: {},
transactions: {},
transactionsStatus: {},
entry: {},
blocks: {},
blocksMeta: {},
accountsDataSlice: [],
ping: undefined ,
commitment: CommitmentLevel . CONFIRMED ,
};
await new Promise (( resolve , reject ) => {
stream . write ( request , ( err ) => {
if ( err === null || err === undefined ) {
console . log ( `Monitoring wallet: ${ WALLET_ADDRESS } \n ` );
resolve ();
} else {
reject ( err );
}
});
});
await streamClosed ;
})();
Example 3: Watch Program Accounts
Track all accounts owned by a specific program:
const PROGRAM_ID = "YourProgramIdHere" ;
const programStats = {
totalUpdates: 0 ,
accounts: new Map (),
createdAccounts: 0 ,
lastReportTime: Date . now ()
};
( async () => {
const client = await getClient ();
const stream = await client . subscribe ();
const streamClosed = new Promise (( resolve , reject ) => {
stream . on ( "error" , ( error ) => console . error ( "Stream error:" , error ));
stream . on ( "end" , () => resolve ());
stream . on ( "close" , () => resolve ());
});
stream . on ( "data" , ( data ) => {
if ( data ?. account ) {
const account = data . account . account ;
programStats . totalUpdates ++ ;
// Track if this is a new account
const isNew = ! programStats . accounts . has ( account . pubkey );
if ( isNew ) {
programStats . createdAccounts ++ ;
}
programStats . accounts . set ( account . pubkey , {
lamports: account . lamports ,
dataSize: account . data ?. length || 0 ,
lastUpdated: Date . now ()
});
console . log ( ` \n [ ${ isNew ? 'NEW' : 'UPDATE' } Account # ${ programStats . totalUpdates } ]` );
console . log ( ` Account: ${ account . pubkey . slice ( 0 , 12 ) } ...` );
console . log ( ` Owner: ${ account . owner . slice ( 0 , 12 ) } ...` );
console . log ( ` Lamports: ${ account . lamports . toLocaleString () } ` );
console . log ( ` Data Size: ${ account . data ?. length || 0 } bytes` );
console . log ( ` Slot: ${ data . account . slot } ` );
if ( isNew ) {
console . log ( ` 🎉 New account created!` );
}
// Report every 50 updates
if ( programStats . totalUpdates % 50 === 0 ) {
const now = Date . now ();
const elapsed = ( now - programStats . lastReportTime ) / 1000 ;
const updatesPerSec = 50 / elapsed ;
// Calculate total lamports across all accounts
let totalLamports = 0 ;
let totalDataSize = 0 ;
programStats . accounts . forEach ( acc => {
totalLamports += acc . lamports ;
totalDataSize += acc . dataSize ;
});
console . log ( ` \n === Program Statistics ===` );
console . log ( ` Total Updates: ${ programStats . totalUpdates } ` );
console . log ( ` Active Accounts: ${ programStats . accounts . size } ` );
console . log ( ` New Accounts: ${ programStats . createdAccounts } ` );
console . log ( ` Updates/sec: ${ updatesPerSec . toFixed ( 1 ) } ` );
console . log ( ` Total Lamports: ${ totalLamports . toLocaleString () } ` );
console . log ( ` Avg Data Size: ${ ( totalDataSize / programStats . accounts . size ). toFixed ( 0 ) } bytes` );
programStats . lastReportTime = now ;
}
}
});
const request = {
accounts: {
programAccounts: {
account: [],
owner: [ PROGRAM_ID ],
filters: []
}
},
slots: {},
transactions: {},
transactionsStatus: {},
entry: {},
blocks: {},
blocksMeta: {},
accountsDataSlice: [],
ping: undefined ,
commitment: CommitmentLevel . CONFIRMED ,
};
await new Promise (( resolve , reject ) => {
stream . write ( request , ( err ) => {
if ( err === null || err === undefined ) {
console . log ( `✅ Monitoring program accounts: ${ PROGRAM_ID } \n ` );
resolve ();
} else {
reject ( err );
}
});
});
await streamClosed ;
})();
Example 4: Monitor with Data Slicing
Only download the data you need to save bandwidth:
// Example: Only get balance data from token accounts
const TOKEN_PROGRAM = "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" ;
const sliceStats = {
totalUpdates: 0 ,
balances: new Map ()
};
( async () => {
const client = await getClient ();
const stream = await client . subscribe ();
const streamClosed = new Promise (( resolve , reject ) => {
stream . on ( "error" , ( error ) => console . error ( "Stream error:" , error ));
stream . on ( "end" , () => resolve ());
stream . on ( "close" , () => resolve ());
});
stream . on ( "data" , ( data ) => {
if ( data ?. account ) {
const account = data . account . account ;
sliceStats . totalUpdates ++ ;
// Extract balance from the sliced data (if available)
if ( account . data && account . data . length >= 8 ) {
try {
const balanceBytes = Buffer . from ( account . data . slice ( 0 , 8 ));
const balance = balanceBytes . readBigUInt64LE ();
const previousBalance = sliceStats . balances . get ( account . pubkey ) || 0 n ;
sliceStats . balances . set ( account . pubkey , balance );
const change = balance - previousBalance ;
console . log ( ` \n [Balance Update # ${ sliceStats . totalUpdates } ]` );
console . log ( ` Account: ${ account . pubkey . slice ( 0 , 12 ) } ...` );
console . log ( ` Balance: ${ balance . toString () } ` );
console . log ( ` Slot: ${ data . account . slot } ` );
} catch ( e ) {
console . error ( "Error parsing balance:" , e );
}
}
}
});
const request = {
accounts: {
tokenAccounts: {
account: [],
owner: [ TOKEN_PROGRAM ],
filters: [
{ dataSize: 165 } // Token account size
]
}
},
slots: {},
transactions: {},
transactionsStatus: {},
entry: {},
blocks: {},
blocksMeta: {},
accountsDataSlice: [
{ offset: 64 , length: 8 } // Only get balance (bytes 64-71)
],
ping: undefined ,
commitment: CommitmentLevel . CONFIRMED ,
};
await new Promise (( resolve , reject ) => {
stream . write ( request , ( err ) => {
if ( err === null || err === undefined ) {
console . log ( `Monitoring token balances \n ` );
resolve ();
} else {
reject ( err );
}
});
});
await streamClosed ;
})();
Understanding Token Account Data
SPL Token accounts are 165 bytes with this structure:
Bytes What It Is
----- ----------
0-31 Mint address (which token)
32-63 Owner address (who owns it)
64-71 Balance (how many tokens)
72-75 Delegate option
76-107 Delegate address
108 Account state
109-112 Is native option
113-120 Is native amount
121-128 Delegated amount
129-132 Close authority option
133-164 Close authority address
Common Filters
Use this to filter account types: filters : [
{ dataSize: 165 } // Token accounts
]
Common sizes:
Token accounts: 165 bytes
Mint accounts: 82 bytes
System accounts: 0 bytes (just SOL)
Filter by Memory Compare (memcmp)
Use this to filter by specific data: filters : [
{
memcmp: {
offset: 0 , // Start at byte 0
bytes: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v" // USDC mint
}
}
]
This checks if bytes at a specific offset match a value.
Use both together: filters : [
{ dataSize: 165 },
{
memcmp: {
offset: 0 ,
bytes: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"
}
}
]
All filters must match (AND logic).
Save Bandwidth with Data Slicing
Only download the parts of account data you need:
// Just get the balance (bytes 64-71) from token accounts
accountsDataSlice : [
{ offset: 64 , length: 8 } // Token balance (u64)
]
// Get multiple slices
accountsDataSlice : [
{ offset: 0 , length: 32 }, // Mint address
{ offset: 32 , length: 32 }, // Owner address
{ offset: 64 , length: 8 } // Balance
]
Use Data Slicing Only request the bytes you need to save bandwidth
Filter Smart Use dataSize and memcmp to reduce updates
Pick Right Commitment CONFIRMED is usually best for most use cases
Watch Your Volume Monitor how many updates you’re receiving
What You Can Build
Monitor wallet or token balances in real-time. Get alerts when balances change significantly.
Track specific wallets and see all their account activity. Great for analyzing behavior patterns.
Monitor all accounts in your program to understand usage, track growth, and detect issues.
Get instant notifications when new accounts are created for your program or specific token mints.
Watch multiple token accounts to track portfolio changes and calculate total values.
Common Questions
How much data will I receive?
It depends on your filters. Watching a single wallet = very little data. Watching all token accounts = a lot of data. Always use filters to limit what you receive.
Can I watch multiple accounts?
Yes! Just add them to the account
array. You can watch hundreds of specific accounts at once.
What's the difference between CONFIRMED and FINALIZED?
CONFIRMED (~400ms): Faster, good for most uses. Supermajority of validators agree.
FINALIZED (~13 seconds): Slower but absolute certainty. Cannot be rolled back.
How do I reduce bandwidth usage?
Use data slicing (accountsDataSlice
) to only get the bytes you need. For example, if you only care about balances, just request bytes 64-71 instead of the full 165-byte account data.
Can I filter by token mint?
Yes! Use a memcmp filter at offset 0 with the mint address. This will only show accounts for that specific token.
What's the difference between account and owner filters?
account : Specific account addresses you want to watch
owner : All accounts owned by a program (like Token Program or your custom program)
Next Steps