Concepts · Graph
Account Graph & Edge Generator.
Catscope models finalized account state as a graph. Edges between accounts are defined by a WebAssembly module you compile — the edge generator.
The Account Graph
On Solana, accounts don't exist in isolation — they reference each other. An Orca liquidity pool account contains the addresses of its token vaults and tick arrays. A lending protocol position account points to a reserve. These relationships are implicit in the account data, but nothing in the base Solana runtime surfaces them.
Catscope makes these relationships explicit as a graph. Each account is a node. The connections between accounts — vault A belongs to this pool, this tick array is linked to that pool — are edges. When your bot subscribes, it subscribes to a node in this graph and receives updates whenever any connected account changes.
In the diagram above, the Orca Pool node is connected to three accounts it depends on. When Token Vault A receives a balance update, any bot subscribed to the pool node is notified immediately — through shared memory, without an RPC call.
The graph only reflects finalized state. Startup snapshots and live slot updates are merged so the view is always consistent. Internally, each node is a u64 ID that Catscope assigns to every Solana public key, and each edge is a u32 type you define.
The Edge Generator
The graph topology doesn't come from Catscope — it comes from you. The edge generator is a WebAssembly module you compile and deploy alongside the Geyser plugin. For every account update, the validator calls into your WASM module with the account data and gets back a list of edges: which other accounts this one connects to, and what type of relationship each edge represents.
This means you control what the graph looks like for your strategy. An Orca-focused bot draws edges from pools to vaults and tick arrays. A lending bot draws edges from positions to reserves. Catscope stores and routes those edges — your WASM defines them.
Subscribing to the Graph
Your bot doesn't subscribe to a list of raw account addresses. It subscribes to a node in the graph. Catscope then walks the edges outward from that node and delivers updates for every connected account — automatically, without you having to enumerate them.
How far outward it walks is controlled by depth. In the Orca example, subscribing to the pool:
- Depth 0 — all connected nodes reachable from the pool, traversed without limit
- Depth 1 — only the subscribed node itself (the pool)
- Depth 2 — the pool plus one hop down: Token Vault A, Token Vault B, and Tick Array
Most strategies subscribe at depth 2 — the pool and the accounts it directly depends on. Depth 0 pulls in everything reachable and should only be used when you need the full subgraph.
The second argument is a Weight filter — pass u32::MAX to receive all edge types. The subscription stays active as long as the returned Subscription object is alive; dropping it cancels the subscription.
// Subscribe to the pool and all downstream accounts (depth 0 = unlimited)
let subscription = graph.subscribe(
pool_account_id, // root node
u32::MAX, // accept all edge types
0, // depth: 0 = all reachable nodes
)?;
// Subscribe to a single account only (depth 1 = node itself)
let sub = graph.subscribe(
signer_account_id,
u32::MAX,
1,
)?;
// The subscription is canceled when this variable drops.
// Store it for as long as you need updates.Note
The graph is built from finalized state only. Startup snapshots and live slot updates are merged so the view is always consistent before any subscription update is delivered.
The reference implementation lives at noncepad/catscope-edge-generator. Build it with:
cargo build --target wasm32-wasip1 --releaseThis produces:
target/wasm32-wasip1/release/catscope_edge_generator.wasmTesting an Edge Generator
The edgegentester CLI runs your compiled edge generator against a real on-chain account and prints the resulting edges to stdout. It fetches the account from an RPC endpoint and feeds it through the WASM module locally — no validator required.
RUST_LOG=debug edgegentester \
--edge=<PATH_TO_WASM> \
--pubkey=<ACCOUNT_PUBKEY> \
--url=<RPC_URL> \
--program-list=<PROGRAM_IDS>Example against mainnet:
RUST_LOG=debug RUST_BACKTRACE=full edgegentester \
--edge=../edge-generator/target/wasm32-wasip1/release/catscope_edge_generator.wasm \
--pubkey=Gb7m4daakbVbrFLR33FKMDVMHAprRZ66CSYt4bpFwUgS \
--url=https://api.mainnet-beta.solana.com \
--program-list=jAR7fwLjxsLkrv8hrsixHSmk9K445dZPFjJosSokJCB,\
PiPeXtStEzi6P6hMQYTDs1gptYTno2TeyACXoCQvC5r,\
whirLbMiicVdio4qvUfM5KAg6Ct8VwpYzGff3uctyCc,\
CPMMoo8L3F4NbTegBCKVNunggL7H1ZpdTHKxQB5qKP1C| Flag | Description |
|---|---|
--edge | Path to compiled WASM edge generator |
--pubkey | Account to test (must not be a program ID) |
--url | RPC endpoint |
--program-list | Comma-separated program IDs. Order must match init() in your edge generator's src/lib.rs |
Program list order
--program-list order must exactly match the order defined in init() inside your edge generator source. A mismatch will produce incorrect edges silently.