> ## Documentation Index
> Fetch the complete documentation index at: https://www.helius.dev/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Token Account (ATA) Filtering

> Catch a wallet's incoming SPL token transfers in LaserStream gRPC and WebSocket streams with tokenAccounts (ATA) expansion — owner-based matching that a plain accountInclude filter misses.

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](/laserstream/grpc) and the [LaserStream WSS](/rpc/websocket/transaction-subscribe).

## 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:

| Value              | Matches                                                                                            | Volume                          | Use 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 change | Higher                          | Full visibility into anything that so much as touches the wallet's token accounts            |
| `"none"`           | No expansion — identical to omitting the field                                                     | —                               | The 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](/laserstream/clients) converts the string to the wire-level `TokenAccountExpansionControlFlag` enum for you (part of `yellowstone-grpc-proto` 12.5.0+).

```typescript theme={"system"}
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](/laserstream/guides/transaction-monitoring) for a fuller example that diffs pre- and post-balances, and the [Subscribe Request reference](/laserstream/grpc) for every transaction filter field.

## Use it in LaserStream WebSocket

The same field is available on the [`transactionSubscribe`](/rpc/websocket/transaction-subscribe) 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`.

```javascript theme={"system"}
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](/laserstream/guides/transaction-monitoring#example-4-watch-a-wallet-incl-token-transfers) shows the full diff.

## Related

<CardGroup cols={2}>
  <Card title="Transaction Monitoring" icon="receipt" href="/laserstream/guides/transaction-monitoring">
    Full filtering strategies and a runnable wallet-watch example over gRPC.
  </Card>

  <Card title="transactionSubscribe (WebSocket)" icon="bolt" href="/rpc/websocket/transaction-subscribe">
    The same `tokenAccounts` field on the WebSocket `transactionSubscribe` method.
  </Card>

  <Card title="Subscribe Request Reference" icon="filter" href="/laserstream/grpc">
    Every transaction filter field, including `tokenAccounts`.
  </Card>

  <Card title="Compressed Filters" icon="layer-group" href="/laserstream/cuckoo-filters">
    Track hundreds of thousands of accounts in one stream.
  </Card>
</CardGroup>
