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.