Skip to main content

Overview

LaserStream supports compressed account filtering via cuckoo filters. Instead of sending an explicit pubkey list in your subscribe request (32 bytes per account), you send a compact probabilistic filter that costs roughly 3–4 bytes per account on the wire. This makes it practical to subscribe to hundreds of thousands of accounts in a single stream — no sharding across connections, no oversized subscribe requests. For example, a filter tracking 500,000 accounts serializes to about 2.1 MB, versus 16 MB as a raw pubkey list — roughly 7.6x smaller. The exact savings depend on how full the filter is: the closer it is to capacity, the fewer bytes per account.

Availability

ClientMinimum versionCuckoo support
LaserStream SDK — Rust (helius-laserstream)0.2.0
LaserStream SDK — JavaScript/TypeScript (helius-laserstream)0.4.0
LaserStream SDK — Go❌ Not yet
Yellowstone gRPC — Rust (yellowstone-grpc-client)13.1.0

When to use cuckoo filters

Tracked accountsRecommended approach
Up to ~10,000Explicit pubkey lists (account: [...]) — simple and exact
~10,000 and aboveCuckoo filter via CompressedAccountFilterSet
Typical use cases: monitoring every holder of a token, tracking all positions in a lending protocol, or watching large wallet sets for a trading or analytics system.

How it works

  1. Build the filter client-side. Insert each tracked pubkey into a CompressedAccountFilterSet. The hash seed is randomized per filter and serialized alongside it, so the server hashes incoming accounts with the same seed your client used.
  2. Attach it to your subscribe request. insert_into_subscribe_request() places the serialized filter into the accounts stream of a standard SubscribeRequest.
  3. The server matches probabilistically. Because the filter is probabilistic, the server may deliver updates for accounts you didn’t track — false positives are bounded at under 1% at full load. There are never false negatives: every update for a tracked account is delivered.
  4. Re-check each update locally — this step is required. Call set.contains(pubkey) on every incoming account before processing it. This check is exact (backed by an internal hash set), so after local filtering you see zero false positives.

Quickstart (Rust)

Add the SDK to your project:
Cargo.toml
[dependencies]
helius-laserstream = "0.2"
tokio = { version = "1", features = ["full"] }
futures = "0.3"
Build a filter, attach it to a subscription, and scrub false positives locally:
main.rs
use {
    futures::StreamExt,
    helius_laserstream::{
        cuckoo::{CompressedAccountFilterSet, Pubkey},
        grpc::{subscribe_update::UpdateOneof, SubscribeRequest},
        subscribe, LaserstreamConfig,
    },
    std::str::FromStr,
};

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    // The exact set of accounts you care about. In production this is
    // typically loaded from your database — hundreds of thousands of keys.
    let tracked: Vec<Pubkey> = [
        "So11111111111111111111111111111111111111112", // Wrapped SOL
        "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", // USDC
        "Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB", // USDT
    ]
    .iter()
    .map(|s| Pubkey::from_str(s).unwrap())
    .collect();

    // Build the cuckoo filter. Size it for your peak tracked-set size.
    let mut set = CompressedAccountFilterSet::with_capacity(500_000)?;
    for pk in &tracked {
        set.insert(*pk)?;
    }
    println!(
        "Tracking {} accounts via cuckoo filter ({} bytes on the wire)",
        set.len(),
        set.to_proto().data.len()
    );

    // Attach the compressed filter to the accounts stream.
    let mut request = SubscribeRequest::default();
    set.insert_into_subscribe_request(&mut request, "tracked_accounts");

    let config = LaserstreamConfig::new(
        "https://laserstream-mainnet-ewr.helius-rpc.com".to_string(), // Choose your closest region
        "YOUR_API_KEY".to_string(), // Replace with your key from https://dashboard.helius.dev/
    );

    let (stream, _handle) = subscribe(config, request);
    tokio::pin!(stream);
    while let Some(message) = stream.next().await {
        match message {
            Ok(update) => {
                if let Some(UpdateOneof::Account(account_update)) = update.update_oneof {
                    if let Some(info) = account_update.account {
                        let pk = Pubkey::try_from(info.pubkey.as_slice()).ok();
                        // Re-check locally: drop server-side false positives.
                        match pk {
                            Some(pk) if set.contains(pk) => {
                                println!(
                                    "tracked account update: {pk} (slot {})",
                                    account_update.slot
                                );
                            }
                            Some(pk) => {
                                println!("(false positive, ignored): {pk}");
                            }
                            None => {}
                        }
                    }
                }
            }
            Err(e) => eprintln!("stream error: {e}"),
        }
    }

    Ok(())
}
A complete runnable version ships with the SDK: rust/examples/cuckoo_account_filter.rs.

Quickstart (JavaScript/TypeScript)

Install the SDK (cuckoo support requires helius-laserstream 0.4.0+):
npm install helius-laserstream
Build the filter, attach it, and re-check each update locally:
import {
  subscribe,
  CommitmentLevel,
  CompressedAccountFilterSet,
  SubscribeUpdate,
  LaserstreamConfig,
} from 'helius-laserstream';

async function main() {
  const config: LaserstreamConfig = {
    apiKey: 'YOUR_API_KEY', // Replace with your key from https://dashboard.helius.dev/
    endpoint: 'https://laserstream-mainnet-ewr.helius-rpc.com', // Choose your closest region
  };

  // The accounts you want to track. In production this is typically loaded
  // from your database — hundreds of thousands of keys.
  const addresses = [
    'So11111111111111111111111111111111111111112', // Wrapped SOL
    'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v', // USDC
    'Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB', // USDT
  ];

  // Build a compact cuckoo filter instead of sending the full pubkey list.
  // Size capacity for your peak tracked-set size.
  const tracked = new CompressedAccountFilterSet(500_000);
  for (const address of addresses) {
    tracked.insert(address);
  }

  // Attach the filter to the request (no explicit account list needed).
  const request: any = { accounts: {}, commitment: CommitmentLevel.CONFIRMED };
  tracked.insertIntoSubscribeRequest(request, 'tracked-accounts');

  const stream = await subscribe(
    config,
    request,
    async (update: SubscribeUpdate) => {
      const pubkey = update.account?.account?.pubkey;
      if (!pubkey) return;
      // Re-check locally: drop server-side false positives. This is exact.
      if (tracked.contains(pubkey)) {
        console.log('tracked account update:', update.account);
      }
    },
    (error: Error) => {
      console.error('Stream error:', error);
    }
  );

  process.on('SIGINT', () => {
    stream.cancel();
    process.exit(0);
  });
}

main().catch(console.error);
A complete runnable version ships with the SDK: javascript/examples/cuckoo-account-sub.ts.

API reference

CompressedAccountFilterSet wraps the raw cuckoo filter together with an exact hash set, so mutations and membership checks are always safe and exact:
MethodBehavior
with_capacity(n)Create a filter sized for n tracked accounts. Size for your peak tracked-set size.
insert(pubkey)Returns Ok(true) if new, Ok(false) if a duplicate, Err(TableFullError) if the filter is at capacity.
remove(pubkey)Removes the account. Safe and exact.
contains(pubkey)Exact membership check — use this to scrub server-side false positives.
insert_into_subscribe_request(&mut request, "label")Attach the filter to the accounts stream of a SubscribeRequest.
to_account_filter() / to_proto()Lower-level conversions for custom request assembly.
is_dirty() / take_dirty()Report whether the set changed since it was last placed into a request — useful for resubscribe cycles.
Method names above use Rust conventions. The JavaScript/TypeScript SDK exposes the same surface in camelCase — new CompressedAccountFilterSet(capacity) instead of with_capacity, insertIntoSubscribeRequest, isDirty, takeDirty, toProto, and so on. In JavaScript insert returns a boolean (true if newly added) and throws TableFullError when the filter is saturated. A pubkey can be passed as a base58 string, raw 32 bytes, or any object with a toBytes() method. Always use CompressedAccountFilterSet rather than the raw CuckooFilter it wraps. The raw filter’s remove() can silently remove the wrong item — a documented footgun of cuckoo filters. The wrapper pairs the filter with an exact hash set, so insert, remove, and contains are always correct.

Capacity sizing

  • Size the filter for the peak number of accounts you expect to track via with_capacity(n).
  • Inserting beyond capacity fails gracefully with a TableFullError — the filter is never corrupted. In practice the table tolerates slight overfill before rejecting inserts, but don’t rely on that headroom.
  • The serialized size is determined by capacity, not by how many accounts you’ve inserted — so an oversized filter wastes wire bytes. Pick a capacity close to your real peak.

Updating the tracked set

When your tracked set changes (new accounts to follow, old ones to drop):
  1. Call insert() / remove() on the CompressedAccountFilterSet.
  2. Check is_dirty() (or consume the flag with take_dirty()) to see whether the filter changed since it was last sent.
  3. If dirty, rebuild the request with insert_into_subscribe_request(). In JavaScript you can re-send it on the same stream with stream.write(request); in Rust, resubscribe with the rebuilt request.

FAQ

No. Cuckoo filters produce false positives (extra updates for untracked accounts) but never false negatives. Every update for a tracked account is delivered.
Under 1% at full load, and typically less when the filter is below capacity. One local contains() call per update filters them out exactly.
The Rust SDK (helius-laserstream 0.2.0+), the JavaScript/TypeScript SDK (helius-laserstream 0.4.0+), and the Yellowstone Rust client (yellowstone-grpc-client 13.1.0+). The Go SDK does not support it yet. See the availability table above.
Yes. Standard account: [...] filters work unchanged and remain the right choice for small account sets (up to roughly 10,000 accounts). See the account subscription guide.

Account Subscriptions

Standard account filtering with owner, datasize, and memcmp filters.

Clients & SDKs

TypeScript, Rust, and Go SDKs with automatic replay and reconnects.