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

# Streaming Token Accounts

> Stream light-token accounts via Laserstream. Same base layout as SPL Token.

<Note>
  This guide is for teams building custom data pipelines (aggregators, market makers).
  If you just need account lookups, use [`get_account_interface`](/light-token/defi/routers) instead.
</Note>

<Accordion title="Agent skill">
  Use the [data-streaming](https://github.com/Lightprotocol/skills/tree/main/skills/data-streaming) agent skill to add Laserstream support to your project:

  ```
  npx skills add Lightprotocol/skills
  ```

  For orchestration, install the [general skill](https://zkcompression.com/skill.md):

  ```bash theme={null}
  npx skills add https://zkcompression.com
  ```
</Accordion>

<Tabs>
  <Tab title="Guide">
    ## Architecture

    Light token accounts share the same base layout as SPL Token (165 bytes), so you can
    use your existing parser. The streaming setup requires two gRPC subscriptions, both
    targeting the Light Token Program:

    | Subscription                                   | Detects                 | How                                      |
    | :--------------------------------------------- | :---------------------- | :--------------------------------------- |
    | Account sub (`owner: cToken...`)               | Hot state + cold-to-hot | Pubkey cache lookup                      |
    | Transaction sub (`account_include: cToken...`) | Hot-to-cold             | Balance heuristic (`pre > 0, post == 0`) |

    The account subscription delivers all state changes while accounts are hot.
    The transaction subscription is needed to detect accounts going cold
    (`compress_and_close` changes the owner to System Program, which the account
    subscription no longer matches).

    ## Parsing

    ```rust theme={null}
    use spl_pod::bytemuck::pod_from_bytes;
    use spl_token_2022_interface::pod::PodAccount; // works for SPL-token, SPL-token-2022, and Light-token

    let parsed: &PodAccount = pod_from_bytes(&data[..165])?;
    ```

    For accounts with extensions, truncate to 165 bytes before parsing.

    ## Streaming

    ```toml Cargo.toml theme={null}
    [dependencies]
    helius-laserstream = "0.1"
    tokio = { version = "1", features = ["full"] }
    futures = "0.3"
    bs58 = "0.5"
    borsh = "0.10"
    light-token-interface = "0.5.0"
    ```

    ```rust theme={null}
    use futures::StreamExt;
    use helius_laserstream::grpc::subscribe_request_filter_accounts_filter::Filter;
    use helius_laserstream::grpc::subscribe_request_filter_accounts_filter_memcmp::Data;
    use helius_laserstream::grpc::{
        SubscribeRequestFilterAccounts, SubscribeRequestFilterAccountsFilter,
        SubscribeRequestFilterAccountsFilterMemcmp, SubscribeRequestFilterTransactions,
    };
    use helius_laserstream::{subscribe, LaserstreamConfig};

    const LIGHT_TOKEN_PROGRAM_ID: &str = "cTokenmWW8bLPjZEBAUgYy3zKxQZW6VKi7bqNFEVv3m";
    const TOKEN_ACCOUNT_SIZE: u64 = 165;
    const ACCOUNT_TYPE_OFFSET: u64 = 165;
    const ACCOUNT_TYPE_TOKEN: u8 = 2;
    ```

    <Steps>
      <Step>
        ### Connect

        <Tabs>
          <Tab title="Mainnet">
            ```rust theme={null}
            let config = LaserstreamConfig::new(
                "https://laserstream-mainnet-ewr.helius-rpc.com".to_string(),
                std::env::var("HELIUS_API_KEY")?,
            );
            ```
          </Tab>

          <Tab title="Devnet">
            ```rust theme={null}
            let config = LaserstreamConfig::new(
                "https://laserstream-devnet-ewr.helius-rpc.com".to_string(),
                std::env::var("HELIUS_API_KEY")?,
            );
            ```
          </Tab>
        </Tabs>
      </Step>

      <Step>
        ### Subscribe

        ```rust theme={null}
        let mut request = helius_laserstream::grpc::SubscribeRequest::default();

        // 1. Account sub: hot state tracking + cold-to-hot detection.
        request.accounts.insert(
            "light_tokens".to_string(),
            SubscribeRequestFilterAccounts {
                owner: vec![LIGHT_TOKEN_PROGRAM_ID.to_string()],
                filters: vec![SubscribeRequestFilterAccountsFilter {
                    filter: Some(Filter::Datasize(TOKEN_ACCOUNT_SIZE)),
                }],
                nonempty_txn_signature: Some(true),
                ..Default::default()
            },
        );
        request.accounts.insert(
            "light_tokens_extended".to_string(),
            SubscribeRequestFilterAccounts {
                owner: vec![LIGHT_TOKEN_PROGRAM_ID.to_string()],
                filters: vec![SubscribeRequestFilterAccountsFilter {
                    filter: Some(Filter::Memcmp(SubscribeRequestFilterAccountsFilterMemcmp {
                        offset: ACCOUNT_TYPE_OFFSET,
                        data: Some(Data::Bytes(vec![ACCOUNT_TYPE_TOKEN])),
                    })),
                }],
                nonempty_txn_signature: Some(true),
                ..Default::default()
            },
        );

        // 2. Transaction sub: hot-to-cold detection.
        request.transactions.insert(
            "light_token_txns".to_string(),
            SubscribeRequestFilterTransactions {
                vote: Some(false),
                failed: Some(false),
                account_include: vec![LIGHT_TOKEN_PROGRAM_ID.to_string()],
                ..Default::default()
            },
        );

        let (stream, _handle) = subscribe(config, request);
        tokio::pin!(stream);
        ```
      </Step>
    </Steps>

    ## Detecting transitions

    ### Hot-to-cold

    For each transaction update, find accounts whose lamport balance dropped to zero.
    The `cache.remove` call ensures only accounts you're already tracking are processed:

    Two data structures:

    * `cache: HashMap<[u8; 32], T>` -- hot account state (for quoting/routing)
    * `cold_cache: HashMap<[u8; 32], AccountInterface>` -- cold accounts with `ColdContext` (for building load instructions)

    ```rust theme={null}
    use helius_laserstream::grpc::subscribe_update::UpdateOneof;

    Some(UpdateOneof::Transaction(tx_update)) => {
        if let Some(ref tx_info) = tx_update.transaction {
            for pubkey in find_closed_accounts(tx_info) {
                if cache.remove(&pubkey).is_some() {
                    // Async: fetch AccountInterface with ColdContext.
                    // Cold accounts are inactive, so this completes well
                    // before anyone tries to swap through them.
                    let rpc = rpc.clone();
                    let cold_cache = cold_cache.clone();
                    tokio::spawn(async move {
                        if let Ok(Some(iface)) = rpc.get_account_interface(&pubkey, None).await {
                            cold_cache.insert(pubkey, iface);
                        }
                    });
                }
            }
        }
    }
    ```

    ```rust theme={null}
    fn find_closed_accounts(
        tx_info: &helius_laserstream::grpc::SubscribeUpdateTransactionInfo,
    ) -> Vec<[u8; 32]> {
        let meta = match &tx_info.meta {
            Some(m) => m,
            None => return vec![],
        };
        let msg = match tx_info.transaction.as_ref().and_then(|t| t.message.as_ref()) {
            Some(m) => m,
            None => return vec![],
        };

        let mut all_keys: Vec<&[u8]> = msg.account_keys.iter().map(|k| k.as_slice()).collect();
        all_keys.extend(meta.loaded_writable_addresses.iter().map(|k| k.as_slice()));
        all_keys.extend(meta.loaded_readonly_addresses.iter().map(|k| k.as_slice()));

        let mut closed = Vec::new();
        for (i, key) in all_keys.iter().enumerate() {
            if key.len() == 32
                && meta.pre_balances.get(i).copied().unwrap_or(0) > 0
                && meta.post_balances.get(i).copied().unwrap_or(1) == 0
            {
                closed.push(<[u8; 32]>::try_from(*key).unwrap());
            }
        }
        closed
    }
    ```

    `cache.remove` filters out unrelated closures in the same transaction. No discriminator
    check is needed -- `compress_and_close` always drains lamports to zero.

    To build transactions that decompress cold accounts, see
    [Router Integration](/light-token/defi/routers).

    ### Cold-to-hot

    When a token account is decompressed, the account subscription delivers the
    re-created account. Match its pubkey against `cold_cache`:

    ```rust theme={null}
    Some(UpdateOneof::Account(account_update)) => {
        if let Some(account) = account_update.account {
            let pubkey: [u8; 32] = account.pubkey.as_slice().try_into().unwrap();
            let parsed: &PodAccount = pod_from_bytes(&account.data[..165])?;

            cold_cache.remove(&pubkey); // no longer cold
            cache.insert(pubkey, *parsed);
        }
    }
    ```

    ## Point queries

    `getAccountInfo` returns null for cold accounts. `get_account_interface()` races
    hot and cold lookups and returns raw account bytes that work with your standard SPL parser:

    ```rust theme={null}
    use light_client::rpc::{LightClient, LightClientConfig, Rpc};
    use spl_pod::bytemuck::pod_from_bytes;
    use spl_token_2022_interface::pod::PodAccount;

    let config = LightClientConfig::new(
        "https://api.devnet.solana.com".to_string(),
        Some("https://photon.helius.com?api-key=YOUR_KEY".to_string()),
    );
    let client = LightClient::new(config).await?;
    let result = client.get_account_interface(&pubkey, None).await?;

    if let Some(account) = result.value {
        let parsed: &PodAccount = pod_from_bytes(&account.data()[..165])?;
        if account.is_cold() {
            // Compressed -- still valid for routing.
        }
    }
    ```

    ## Data layout

    165 bytes base, identical to SPL Token Account.

    | Field              | Offset | Size |
    | :----------------- | :----- | :--- |
    | `mint`             | 0      | 32   |
    | `owner`            | 32     | 32   |
    | `amount`           | 64     | 8    |
    | `delegate`         | 72     | 36   |
    | `state`            | 108    | 1    |
    | `is_native`        | 109    | 12   |
    | `delegated_amount` | 121    | 8    |
    | `close_authority`  | 129    | 36   |
    | `account_type`     | 165    | 1    |

    `account_type = 2` at byte 165 indicates extensions follow (borsh-encoded `Option<Vec<ExtensionStruct>>`).

    <Expandable title="Light extensions">
      These are not needed for indexing or trading.

      ```rust theme={null}
      use borsh::BorshDeserialize;
      use light_token_interface::state::{Token, ExtensionStruct};

      let token = Token::deserialize(&mut data.as_slice())?;

      if let Some(exts) = &token.extensions {
          for ext in exts {
              if let ExtensionStruct::Compressible(info) = ext {
                  // info.compression_authority, info.rent_sponsor, info.last_claimed_slot
              }
          }
      }
      ```

      | Variant                                                       | Description                                                   |
      | :------------------------------------------------------------ | :------------------------------------------------------------ |
      | `TokenMetadata(TokenMetadata)`                                | Name, symbol, URI, additional metadata                        |
      | `PausableAccount(PausableAccountExtension)`                   | Marker: mint is pausable (no data; pause state lives on mint) |
      | `PermanentDelegateAccount(PermanentDelegateAccountExtension)` | Marker: mint has permanent delegate                           |
      | `TransferFeeAccount(TransferFeeAccountExtension)`             | Withheld fees from transfers                                  |
      | `TransferHookAccount(TransferHookAccountExtension)`           | Marker: mint has transfer hook                                |
      | `CompressedOnly(CompressedOnlyExtension)`                     | Compressed-only token (stores delegated amount)               |
      | `Compressible(CompressibleExtension)`                         | Compression config: authority, rent sponsor, timing           |

      Source: [`light-token-interface`](https://github.com/Lightprotocol/light-protocol/tree/main/program-libs/token-interface/src/state/extensions)
    </Expandable>

    <Card title="Streaming Mint Accounts" icon="chevron-right" color="#0066ff" href="/light-token/streaming/mints" horizontal />
  </Tab>

  <Tab title="AI Prompt">
    <Prompt description="Stream light-token accounts via Laserstream gRPC" actions={["copy", "cursor"]}>
      {`---
            description: Stream light-token accounts via Laserstream gRPC
            allowed-tools: Bash, Read, Write, Edit, Glob, Grep, WebFetch, AskUserQuestion, Task, TaskCreate, TaskGet, TaskList, TaskUpdate, TaskOutput, mcp__deepwiki, mcp__zkcompression
            ---

            ## Stream light-token accounts via Laserstream gRPC

            Context:
            - Guide: https://zkcompression.com/light-token/streaming/tokens
            - Skills and resources index: https://zkcompression.com/skill.md
            - Dedicated skill: https://github.com/Lightprotocol/skills/tree/main/skills/data-streaming
            - Crates: helius-laserstream, light-token-interface, spl-pod, spl-token-2022-interface, borsh, futures
            - Mint accounts streaming: https://zkcompression.com/light-token/streaming/mints
            - Point queries: light-client (LightClient, get_account_interface)

            Key APIs: LaserstreamConfig, subscribe(), PodAccount (pod_from_bytes), LightClient::get_account_interface()

            ### 1. Index project
            - Grep \`helius_laserstream|laserstream|subscribe|PodAccount|pod_from_bytes|spl_token_2022_interface|cTokenmWW8bLPjZEBAUgYy3zKxQZW6VKi7bqNFEVv3m\` across src/
            - Glob \`**/*.rs\` and \`**/Cargo.toml\` for project structure
            - Identify: existing gRPC streaming setup, token account caching, SPL parser usage
            - Read Cargo.toml — note existing dependencies
            - Task subagent (Grep/Read/WebFetch) if project has multiple crates to scan in parallel

            ### 2. Read references
            - WebFetch the guide above — follow the Steps (Connect, Subscribe) and the transition detection sections
            - WebFetch skill.md — check for a dedicated skill and resources matching this task
            - TaskCreate one todo per phase below to track progress

            ### 3. Clarify intention
            - AskUserQuestion: what is the goal? (new streaming pipeline for token accounts, add to existing pipeline, integrate cold/hot detection for routing)
            - AskUserQuestion: mainnet or devnet?
            - AskUserQuestion: do you need point queries (get_account_interface) in addition to streaming?
            - Summarize findings and wait for user confirmation before implementing

            ### 4. Create plan
            - Based on steps 1–3, draft an implementation plan
            - Follow the guide's structure: Connect → Subscribe (account + transaction subs) → Detect Transitions (hot-to-cold, cold-to-hot) → Point Queries (optional)
            - Token accounts use the same 165-byte SPL layout — existing SPL parsers work directly
            - If anything is unclear or ambiguous, loop back to step 3 (AskUserQuestion)
            - Present the plan to the user for approval before proceeding

            ### 5. Implement
            - Add deps if missing: Bash \`cargo add helius-laserstream@0.1 light-token-interface@0.5 spl-pod spl-token-2022-interface borsh@0.10 futures@0.3 bs58@0.5 tokio --features full\`
            - For point queries, also: Bash \`cargo add light-client@0.23 --features v2\`
            - Follow the guide and the approved plan
            - Write/Edit to create or modify files
            - TaskUpdate to mark each step done

            ### 6. Verify
            - Bash \`cargo check\`
            - Bash \`cargo test\` if tests exist
            - TaskUpdate to mark complete

            ### Tools
            - mcp__zkcompression__SearchLightProtocol("<query>") for API details
            - mcp__deepwiki__ask_question("Lightprotocol/light-protocol", "<q>") for architecture
            - Task subagent with Grep/Read/WebFetch for parallel lookups
            - TaskList to check remaining work`}
    </Prompt>

    ```text theme={null}
    ---
    description: Stream light-token accounts via Laserstream gRPC
    allowed-tools: Bash, Read, Write, Edit, Glob, Grep, WebFetch, AskUserQuestion, Task, TaskCreate, TaskGet, TaskList, TaskUpdate, TaskOutput, mcp__deepwiki, mcp__zkcompression
    ---

    ## Stream light-token accounts via Laserstream gRPC

    Context:
    - Guide: https://zkcompression.com/light-token/streaming/tokens
    - Skills and resources index: https://zkcompression.com/skill.md
    - Dedicated skill: https://github.com/Lightprotocol/skills/tree/main/skills/data-streaming
    - Crates: helius-laserstream, light-token-interface, spl-pod, spl-token-2022-interface, borsh, futures
    - Mint accounts streaming: https://zkcompression.com/light-token/streaming/mints
    - Point queries: light-client (LightClient, get_account_interface)

    Key APIs: LaserstreamConfig, subscribe(), PodAccount (pod_from_bytes), LightClient::get_account_interface()

    ### 1. Index project
    - Grep `helius_laserstream|laserstream|subscribe|PodAccount|pod_from_bytes|spl_token_2022_interface|cTokenmWW8bLPjZEBAUgYy3zKxQZW6VKi7bqNFEVv3m` across src/
    - Glob `**/*.rs` and `**/Cargo.toml` for project structure
    - Identify: existing gRPC streaming setup, token account caching, SPL parser usage
    - Read Cargo.toml — note existing dependencies
    - Task subagent (Grep/Read/WebFetch) if project has multiple crates to scan in parallel

    ### 2. Read references
    - WebFetch the guide above — follow the Steps (Connect, Subscribe) and the transition detection sections
    - WebFetch skill.md — check for a dedicated skill and resources matching this task
    - TaskCreate one todo per phase below to track progress

    ### 3. Clarify intention
    - AskUserQuestion: what is the goal? (new streaming pipeline for token accounts, add to existing pipeline, integrate cold/hot detection for routing)
    - AskUserQuestion: mainnet or devnet?
    - AskUserQuestion: do you need point queries (get_account_interface) in addition to streaming?
    - Summarize findings and wait for user confirmation before implementing

    ### 4. Create plan
    - Based on steps 1–3, draft an implementation plan
    - Follow the guide's structure: Connect → Subscribe (account + transaction subs) → Detect Transitions (hot-to-cold, cold-to-hot) → Point Queries (optional)
    - Token accounts use the same 165-byte SPL layout — existing SPL parsers work directly
    - If anything is unclear or ambiguous, loop back to step 3 (AskUserQuestion)
    - Present the plan to the user for approval before proceeding

    ### 5. Implement
    - Add deps if missing: Bash `cargo add helius-laserstream@0.1 light-token-interface@0.5 spl-pod spl-token-2022-interface borsh@0.10 futures@0.3 bs58@0.5 tokio --features full`
    - For point queries, also: Bash `cargo add light-client@0.23 --features v2`
    - Follow the guide and the approved plan
    - Write/Edit to create or modify files
    - TaskUpdate to mark each step done

    ### 6. Verify
    - Bash `cargo check`
    - Bash `cargo test` if tests exist
    - TaskUpdate to mark complete

    ### Tools
    - mcp__zkcompression__SearchLightProtocol("<query>") for API details
    - mcp__deepwiki__ask_question("Lightprotocol/light-protocol", "<q>") for architecture
    - Task subagent with Grep/Read/WebFetch for parallel lookups
    - TaskList to check remaining work
    ```
  </Tab>
</Tabs>

***

## Didn't find what you were looking for?

<Callout type="info">
  Reach out! [Telegram](https://t.me/swen_light) | [email](mailto:support@lightprotocol.com) | [Discord](https://discord.com/invite/7cJ8BhAXhu)
</Callout>
