> ## 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 over WebSocket

> Catch a wallet's incoming SPL token transfers in LaserStream WebSocket streams with the tokenAccounts filter on transactionSubscribe — owner-based matching that a plain accountInclude filter misses.

The `tokenAccounts` filter on the [`transactionSubscribe`](/docs/rpc/websocket/transaction-subscribe) WebSocket method lets a subscription match activity on the **associated token accounts (ATAs) a wallet owns**, not just transactions where the wallet's pubkey appears directly. The same filter is available over gRPC — see [Token Account (ATA) Filtering](/docs/laserstream/token-account-filtering) for the gRPC version.

## 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 the subscription 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 `transactionSubscribe`

`tokenAccounts` is 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 example above shows the filtering step.

## Related

<CardGroup cols={2}>
  <Card title="transactionSubscribe" icon="bolt" href="/docs/rpc/websocket/transaction-subscribe">
    Every `transactionSubscribe` filter and option, including `tokenAccounts`.
  </Card>

  <Card title="Token Account Filtering (gRPC)" icon="coins" href="/docs/laserstream/token-account-filtering">
    The same `tokenAccounts` expansion on LaserStream gRPC transaction filters.
  </Card>

  <Card title="notifyOn Filtering" icon="filter" href="/docs/rpc/websocket/notify-on-filtering">
    Skip no-op account updates on `accountSubscribe` and `programSubscribe`.
  </Card>

  <Card title="WebSocket Quickstart" icon="rocket" href="/docs/rpc/websocket/quickstart">
    Connect to LaserStream WebSocket and stream your first events.
  </Card>
</CardGroup>
