Skip to main content

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\nShutting 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) || 0n;
          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)
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
]

Tips for Better Performance

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

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.
Yes! Just add them to the account array. You can watch hundreds of specific accounts at once.
  • CONFIRMED (~400ms): Faster, good for most uses. Supermajority of validators agree.
  • FINALIZED (~13 seconds): Slower but absolute certainty. Cannot be rolled back.
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.
Yes! Use a memcmp filter at offset 0 with the mint address. This will only show accounts for that specific token.
  • account: Specific account addresses you want to watch
  • owner: All accounts owned by a program (like Token Program or your custom program)

Next Steps

I