| Event | Description |
|---|---|
| New mints | Raw mint data |
| Mint updates | Supply changes, authority changes |
| TokenMetadata | Name, symbol, URI, additional_metadata |
| Cold/hot transitions | Mint compressed or decompressed |
This guide is for teams building custom data pipelines (aggregators, market makers).
If you just need account lookups, use
get_account_interface instead.Agent skill
Agent skill
Use the data-streaming agent skill to add Laserstream support to your project:For orchestration, install the general skill:
- Claude Code
- Cursor
- Any Agent
Add the marketplace and install:
/plugin marketplace add Lightprotocol/skills
/plugin install solana-rent-free-dev
- Open Settings (Cmd+Shift+J / Ctrl+Shift+J)
- Navigate to Rules & Commands → Project Rules → Add Rule → Remote Rule (GitHub)
- Enter:
https://github.com/Lightprotocol/skills.git
npx skills add Lightprotocol/skills
npx skills add https://zkcompression.com
- Guide
- AI Prompt
Architecture
Light mints are Solana accounts owned by the Light Token Program. The streaming setup requires two gRPC subscriptions:| Subscription | Detects | How |
|---|---|---|
Account sub (owner: cToken..., account_type == 1) | Hot state + cold-to-hot | Pubkey cache lookup |
Transaction sub (account_include: cToken...) | Hot-to-cold | Balance heuristic (pre > 0, post == 0) |
CompressAndCloseMint changes the owner to System Program, which the account
subscription no longer matches).Setup
Cargo.toml
[dependencies]
helius-laserstream = "0.1"
tokio = { version = "1", features = ["full"] }
futures = "0.3"
bs58 = "0.5"
borsh = "0.10"
light-token-interface = "0.3.0"
use borsh::BorshDeserialize;
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};
use light_token_interface::state::{ExtensionStruct, Mint};
const LIGHT_TOKEN_PROGRAM_ID: &str = "cTokenmWW8bLPjZEBAUgYy3zKxQZW6VKi7bqNFEVv3m";
const ACCOUNT_TYPE_OFFSET: u64 = 165;
1
Connect
- Mainnet
- Devnet
let config = LaserstreamConfig::new(
"https://laserstream-mainnet-ewr.helius-rpc.com".to_string(),
std::env::var("HELIUS_API_KEY")?,
);
let config = LaserstreamConfig::new(
"https://laserstream-devnet-ewr.helius-rpc.com".to_string(),
std::env::var("HELIUS_API_KEY")?,
);
2
Subscribe
let mut request = helius_laserstream::grpc::SubscribeRequest::default();
// 1. Account sub: mint state tracking + cold-to-hot detection.
// account_type == 1 (Mint) at byte offset 165.
request.accounts.insert(
"light_mints".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![1])),
})),
}],
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);
3
Deserialize mint accounts
use helius_laserstream::grpc::subscribe_update::UpdateOneof;
Some(UpdateOneof::Account(account_update)) => {
if let Some(account_info) = account_update.account {
let pubkey: [u8; 32] = account_info.pubkey.as_slice().try_into().unwrap();
match Mint::deserialize(&mut account_info.data.as_slice()) {
Ok(mint) => {
cold_cache.remove(&pubkey); // no longer cold
cache.insert(pubkey, mint);
}
Err(e) => {
eprintln!(
"Failed to deserialize mint {}: {}",
bs58::encode(&pubkey).into_string(),
e
);
}
}
}
}
4
Detect mints going cold
Two data structures:cache: HashMap<[u8; 32], T>— hot account state (for quoting/routing)cold_cache: HashMap<[u8; 32], AccountInterface>— cold accounts withColdContext(for building load instructions)
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);
}
});
}
}
}
}
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.5
Extract TokenMetadata
fn extract_metadata(mint: &Mint) -> Option<(String, String, String)> {
let extensions = mint.extensions.as_ref()?;
for ext in extensions {
if let ExtensionStruct::TokenMetadata(m) = ext {
let name = String::from_utf8_lossy(&m.name).to_string();
let symbol = String::from_utf8_lossy(&m.symbol).to_string();
let uri = String::from_utf8_lossy(&m.uri).to_string();
return Some((name, symbol, uri));
}
}
None
}
Data Layouts
Mint
#[repr(C)]
pub struct Mint {
pub base: BaseMint,
pub metadata: MintMetadata,
pub reserved: [u8; 16],
pub account_type: u8,
pub compression: CompressionInfo,
pub extensions: Option<Vec<ExtensionStruct>>,
}
#[repr(C)]
pub struct BaseMint {
pub mint_authority: Option<Pubkey>,
pub supply: u64,
pub decimals: u8,
pub is_initialized: bool,
pub freeze_authority: Option<Pubkey>,
}
#[repr(C)]
pub struct MintMetadata {
pub version: u8,
pub mint_decompressed: bool,
pub mint: Pubkey,
pub mint_signer: [u8; 32],
pub bump: u8,
}
TokenMetadata
#[repr(C)]
pub struct TokenMetadata {
pub update_authority: Pubkey, // [0u8; 32] = immutable
pub mint: Pubkey,
pub name: Vec<u8>,
pub symbol: Vec<u8>,
pub uri: Vec<u8>,
pub additional_metadata: Vec<AdditionalMetadata>,
}
pub struct AdditionalMetadata {
pub key: Vec<u8>,
pub value: Vec<u8>,
}
Streaming Token Accounts
Stream light-mint accounts and metadata via Laserstream gRPC
---
description: Stream light-mint accounts and metadata 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-mint accounts and metadata via Laserstream gRPC
Context:
- Guide: https://zkcompression.com/light-token/toolkits/for-streaming-mints
- 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, borsh, futures
- Token accounts streaming: https://zkcompression.com/light-token/toolkits/for-streaming-tokens
Key APIs: LaserstreamConfig, subscribe(), Mint::deserialize(), ExtensionStruct::TokenMetadata
### 1. Index project
- Grep `helius_laserstream|laserstream|subscribe|StreamExt|light_token_interface|Mint|cTokenmWW8bLPjZEBAUgYy3zKxQZW6VKi7bqNFEVv3m` across src/
- Glob `**/*.rs` and `**/Cargo.toml` for project structure
- Identify: existing gRPC streaming setup, account caching, deserialization logic
- 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, Deserialize, Detect cold, Extract metadata)
- 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 mints, add mint streaming to existing token streaming, specific use case like metadata indexing)
- AskUserQuestion: mainnet or devnet?
- AskUserQuestion: do you need cold/hot transition detection, or just live mint state?
- 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 step order: Connect → Subscribe (account + transaction subs) → Deserialize Mint → Detect Cold → Extract Metadata
- 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.3 borsh@0.10 futures@0.3 bs58@0.5 tokio --features full`
- 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