I spent way too long polling getSignaturesForAddress in a loop. Every 3 seconds, hitting the RPC, checking if there’s a new transaction. It worked, but it was ugly and slow.
Then I found out you can just subscribe to account changes over WebSocket. No polling. No wasted requests. You get notified the moment something happens.
Here’s exactly how I do it now.
1. Set Up a WebSocket Connection
@solana/web3.js has built-in WebSocket support. You don’t need a separate library.
import { Connection, PublicKey } from "@solana/web3.js";
const connection = new Connection(
"https://api.mainnet-beta.solana.com",
{
wsEndpoint: "wss://api.mainnet-beta.solana.com",
commitment: "confirmed",
}
);
That’s your entry point. The wsEndpoint is what makes real-time tracking possible.
2. Subscribe to Account Changes
Pick any wallet address and subscribe:
const wallet = new PublicKey("TARGET_WALLET_ADDRESS_HERE");
const subId = connection.onAccountChange(wallet, (accountInfo, context) => {
console.log("SOL balance changed:", accountInfo.lamports / 1e9, "SOL");
console.log("Slot:", context.slot);
});
Every time that wallet’s SOL balance changes, your callback fires. Immediately. No delay.
3. Track Token Movements Too
SOL changes are easy. But most of the interesting stuff happens with SPL tokens. For that, you subscribe to token accounts instead.
First, find all token accounts for a wallet:
import { TOKEN_PROGRAM_ID } from "@solana/spl-token";
const tokenAccounts = await connection.getTokenAccountsByOwner(wallet, {
programId: TOKEN_PROGRAM_ID,
});
for (const { pubkey } of tokenAccounts.value) {
connection.onAccountChange(pubkey, (info) => {
// Token account data changed — parse the new balance
const data = Buffer.from(info.data);
const amount = data.readBigUInt64LE(64);
console.log(`Token account ${pubkey.toBase58()} new raw amount: ${amount}`);
});
}
Now you’re watching every token account that wallet owns. When they buy, sell, or receive tokens — you see it.
4. Watch for New Transactions with onLogs
Sometimes you don’t care about balance changes. You want to see every transaction a wallet touches. onLogs is perfect for that.
connection.onLogs(wallet, (logs, context) => {
console.log("New tx:", logs.signature);
console.log("Logs:", logs.logs);
if (logs.err) {
console.log("Transaction failed:", logs.err);
}
}, "confirmed");
This fires for every transaction involving that wallet. You get the signature and the program logs right away — no extra RPC call needed.
5. Don’t Forget to Clean Up
WebSocket subscriptions stay open until you close them. If you’re tracking multiple wallets, this matters.
// Remove a subscription when you're done
await connection.removeAccountChangeListener(subId);
I learned this the hard way. Left 200+ subscriptions running and my RPC provider wasn’t happy.
6. Handle Disconnections
WebSockets drop. It happens. Your code needs to handle it.
function createMonitor(walletAddress: string) {
const wallet = new PublicKey(walletAddress);
const connect = () => {
const conn = new Connection("https://api.mainnet-beta.solana.com", {
wsEndpoint: "wss://api.mainnet-beta.solana.com",
commitment: "confirmed",
});
conn.onAccountChange(wallet, (info) => {
console.log("Balance:", info.lamports / 1e9, "SOL");
});
// Reconnect on close
conn["_rpcWebSocket"].on("close", () => {
console.log("WebSocket closed. Reconnecting...");
setTimeout(connect, 2000);
});
};
connect();
}
Not the prettiest code. But it works, and that’s what matters when you’re tracking wallets at 3am.
7. Putting It All Together
Here’s a minimal but complete wallet monitor:
import { Connection, PublicKey } from "@solana/web3.js";
import { TOKEN_PROGRAM_ID } from "@solana/spl-token";
const RPC = "https://api.mainnet-beta.solana.com";
const WS = "wss://api.mainnet-beta.solana.com";
async function monitorWallet(address: string) {
const connection = new Connection(RPC, {
wsEndpoint: WS,
commitment: "confirmed",
});
const wallet = new PublicKey(address);
// Watch SOL balance
connection.onAccountChange(wallet, (info) => {
console.log(`[SOL] ${info.lamports / 1e9} SOL`);
});
// Watch all token accounts
const tokens = await connection.getTokenAccountsByOwner(wallet, {
programId: TOKEN_PROGRAM_ID,
});
for (const { pubkey } of tokens.value) {
connection.onAccountChange(pubkey, () => {
console.log(`[TOKEN] ${pubkey.toBase58()} changed`);
});
}
// Watch transaction logs
connection.onLogs(wallet, (logs) => {
console.log(`[TX] ${logs.signature}`);
}, "confirmed");
console.log(`Monitoring ${address}...`);
}
monitorWallet("WALLET_ADDRESS_HERE");
Run it. Point it at any wallet. You’ll see every SOL change, token change, and transaction as it happens.
When to Use This vs Yellowstone gRPC
If you’re tracking a handful of wallets, WebSocket subscriptions are perfect. Simple, built-in, no extra dependencies.
If you’re tracking hundreds of wallets or need to filter by program, Yellowstone gRPC is better — I covered that in my transaction parsing post.
Pick the right tool for the job. For most wallet monitoring use cases, what I showed here is more than enough.