Skip to main content
LaserStream’s tokenAccounts filter lets a transaction subscription match activity on the associated token accounts (ATAs) a wallet owns, not just transactions where the wallet’s pubkey appears directly. It works the same way in both LaserStream gRPC and the LaserStream WSS.

The problem: plain account filters miss incoming token transfers

When you watch a wallet with accountInclude: [wallet], you only match transactions where that wallet pubkey appears in the transaction’s account keys. A common case slips through: when someone sends the wallet an SPL token (USDC, for example), the transfer touches the wallet’s associated token account (ATA) — a separate program-derived address — not the wallet pubkey itself. So a plain accountInclude: [wallet] subscription never sees incoming token transfers. You would have to enumerate every ATA the wallet owns up front and add each one to the filter — but ATAs are created on demand (one per mint), so you can’t know the full set in advance.

How tokenAccounts expansion works

Set tokenAccounts on a transaction filter to expand matching so an accountInclude wallet also matches transactions that touch a token account it owns. Matching is owner-based: LaserStream resolves the token accounts owned by your accountInclude addresses at match time, so it catches any token account the wallet owns — including non-canonical ones — not just the derived ATA address. You never have to list the ATAs yourself. Subscriptions that omit tokenAccounts behave exactly as before, so it’s safe to add to an existing filter.

Expansion modes

tokenAccounts takes one of three string values:
ValueMatchesVolumeUse it for
"balanceChanged"Transactions where an owned token balance actually changed (or its token account was closed)Lower — the recommended default”Tell me when money actually moved” — deposits, withdrawals, swaps that settle to the wallet
"all"Any transaction that references a token account the wallet owns, even if the balance didn’t changeHigherFull visibility into anything that so much as touches the wallet’s token accounts
"none"No expansion — identical to omitting the fieldThe default
Start with "balanceChanged". It captures real fund movement at a fraction of the volume of "all".

Use it in LaserStream gRPC

Add tokenAccounts to a transaction filter in your SubscribeRequest. The Helius LaserStream SDK converts the string to the wire-level TokenAccountExpansionControlFlag enum for you (part of yellowstone-grpc-proto 12.5.0+).
import { subscribe, CommitmentLevel, LaserstreamConfig, SubscribeRequest } from 'helius-laserstream';
import bs58 from 'bs58';

const wallet = '<WALLET_PUBKEY>';

const subscriptionRequest: SubscribeRequest = {
  transactions: {
    "wallet-activity": {
      accountInclude: [wallet],
      accountExclude: [],
      accountRequired: [],
      vote: false,
      failed: false,
      tokenAccounts: "balanceChanged" // also match the wallet's ATAs
    }
  },
  commitment: CommitmentLevel.CONFIRMED,
  accounts: {}, slots: {}, transactionsStatus: {},
  blocks: {}, blocksMeta: {}, entry: {}, accountsDataSlice: [],
};

const config: LaserstreamConfig = {
  apiKey: 'YOUR_API_KEY',
  endpoint: 'https://laserstream-mainnet-ewr.helius-rpc.com',
};

await subscribe(config, subscriptionRequest, async (data) => {
  if (!data.transaction?.transaction) return;
  const tx = data.transaction.transaction;
  // Token balances this wallet owns that changed in the tx
  const owned = (tx.meta?.postTokenBalances || []).filter((b: any) => b.owner === wallet);
  console.log(bs58.encode(tx.signature), owned);
}, async (error) => {
  console.error('Stream error:', error);
});
See the Transaction Monitoring guide for a fuller example that diffs pre- and post-balances, and the Subscribe Request reference for every transaction filter field.

Use it in LaserStream WebSocket

The same field is available on the transactionSubscribe WebSocket method — a Helius extension to the standard Solana WebSocket API. An invalid value returns JSON-RPC error -32602: Invalid tokenAccounts value '<x>', expected one of: none, balanceChanged, all.
const ws = new WebSocket('wss://mainnet.helius-rpc.com/?api-key=<API_KEY>');

ws.on('open', () => {
  ws.send(JSON.stringify({
    jsonrpc: '2.0',
    id: 1,
    method: 'transactionSubscribe',
    params: [
      {
        accountInclude: ['<WALLET_PUBKEY>'],
        tokenAccounts: 'balanceChanged' // also match the wallet's ATAs
      },
      { commitment: 'confirmed', encoding: 'jsonParsed', maxSupportedTransactionVersion: 0 }
    ]
  }));
  setInterval(() => ws.ping(), 30_000);
});

ws.on('message', (data) => {
  const msg = JSON.parse(data.toString());
  const result = msg.params?.result;
  if (!result) return;
  // Token balances this wallet owns that changed in the tx
  const owned = (result.transaction.meta.postTokenBalances || [])
    .filter((b) => b.owner === '<WALLET_PUBKEY>');
  console.log(result.signature, owned);
});

Reading what matched

Once a transaction matches via ATA expansion, the wallet’s token movement lives in the transaction’s meta.postTokenBalances and meta.preTokenBalances. Filter those entries by owner to isolate the balances your wallet actually owns, then diff preTokenBalances against postTokenBalances on the same accountIndex to see how much each mint moved. The examples above show the filtering step; the Transaction Monitoring guide shows the full diff.

Transaction Monitoring

Full filtering strategies and a runnable wallet-watch example over gRPC.

transactionSubscribe (WebSocket)

The same tokenAccounts field on the WebSocket transactionSubscribe method.

Subscribe Request Reference

Every transaction filter field, including tokenAccounts.

Compressed Filters

Track hundreds of thousands of accounts in one stream.