Official Rust client for the CabalSpy API: a realtime multichain data layer for labeled wallets, covering Solana, Base, BNB Chain, Ethereum and Robinhood Chain.
Track what Key Opinion Leaders, smart money wallets and whales are actually buying — with PnL, holder data, cluster signals, bundle detection and our curated calls.
What you can build with it: copy-trading bots, KOL leaderboards, memecoin alert systems, wallet analytics dashboards, bundle and sniper detection, portfolio and PnL trackers.
[dependencies]
cabalspy = "0.3"
tokio = { version = "1", features = ["macros", "rt-multi-thread"] }use cabalspy::{CabalSpy, Chain, LeaderboardOpts, Period, SignalMode, SignalOpts, WalletType};
#[tokio::main]
async fn main() -> Result<(), cabalspy::Error> {
let client = CabalSpy::from_env()?; // reads CABALSPY_API_KEY
let wallet = client.wallets().lookup("As7HjL7dzzvbRbaD3WCun47robib2kmAKRXMvjHkSMB5").await?;
println!("{:?}", wallet.data);
let board = client.wallets().leaderboard(Chain::Solana, LeaderboardOpts {
wallet_type: Some(WalletType::Kol),
period: Some(Period::D7),
limit: Some(25),
..Default::default()
}).await?;
let signals = client.signals().list(
Chain::Solana, WalletType::Kol, SignalMode::Cluster,
SignalOpts { min_wallets: Some(5), hours: Some(1), ..Default::default() },
).await?;
for signal in &signals.data.signals {
println!("{:?} {:?}", signal.token.token_name, signal.window.wallet_count);
}
Ok(())
}The public demo key needs no account. CabalSpy::demo() uses it against the normal API:
let client = cabalspy::CabalSpy::demo()?; // api key "demo"
let kols = client.wallets().list(Chain::Solana, WalletType::Kol, None, None).await?;
println!("{:?} requests left today", kols.demo.and_then(|d| d.remaining_today));Or with no key at all:
curl "https://demo-api.cabalspy.xyz/v1/wallets?blockchain=solana&type=kol"- 20 requests per IP per UTC day, shared between REST and websocket (each connection counts as one)
- Data delayed by 15 minutes, at most 5 rows per list, no pagination
- Websocket: one connection per IP, up to 3 subscriptions, closed after 30 minutes
- Every response carries a
demoblock (Envelope::demo) withremaining_todayand upgrade links - Once the budget is spent, requests fail with
Error::DemoLimit { resets_in_seconds, .. }
For realtime data, get a free test key (1,000 requests per month) at apidashboard.cabalspy.xyz, or pay per call with x402.
| Chain | Variant | Currency | Wallet types |
|---|---|---|---|
| Solana | Chain::Solana |
SOL | Kol, Smart, Whale |
| BNB Chain | Chain::Bnb |
BNB | Kol, Smart |
| Base | Chain::Base |
ETH | Kol, Smart |
| Ethereum | Chain::Eth |
ETH | Kol |
| Robinhood Chain | Chain::Rh |
ETH | Kol, Smart |
Impossible combinations are rejected before a request goes out, so a mistake costs no credits:
let result = client.wallets().list(Chain::Eth, WalletType::Whale, None, None).await;
// Err(Error::InvalidRequest { message: "eth has no whale wallets; it supports: kol" })The deepest coverage of the five. Solana is the only chain with WalletType::Whale, live market cap,
pump.fun bonding curve progress, and the bundle endpoint that detects KOL wallets buying through Jito
bundles alongside side wallets they control.
let bundles = client.bundle().get("MINT_ADDRESS").await?;
for entry in &bundles.data.bundles {
println!("{:?} confidence {:?}", entry.kol_wallet, entry.confidence);
}Base carries both Kol and Smart wallets. Addresses are EVM format, values are denominated in ETH.
let feed = client.transactions()
.latest(Chain::Base, WalletType::Smart, FeedOpts { limit: Some(50), ..Default::default() })
.await?;Kol and Smart wallets, denominated in BNB.
let board = client.wallets()
.leaderboard(Chain::Bnb, LeaderboardOpts { period: Some(Period::D7), ..Default::default() })
.await?;Ethereum mainnet carries Kol wallets only; smart money signals are unavailable there.
Robinhood Chain is Robinhood's Ethereum L2 on the Arbitrum Orbit stack, with ETH as the gas token.
Both Kol and Smart wallets are tracked.
let volume = client.transactions()
.volume(Chain::Rh, WalletType::Kol, FeedOpts { hours: Some(24), ..Default::default() })
.await?;Market cap availability differs between REST and websocket.
On REST,
market_cap,price,unrealized_pnl_*andremaining_*are populated for Solana only; elsewhere they areNone. Realized PnL, invested amounts, holdings and counters work on every chain. SeeChain::has_rest_marketcap().The gateway populates them on every chain, and unlike REST it does not zero the unrealized values once a position is fully sold.
The curated CabalSpy calls, the same ones posted in Discord, with the wallets behind each call and
how it performed over the 24 hours after it. Chains Solana, Bnb, Eth and Rh (Base has none);
tiers Gamble, Moderate, Good, Big and Whale (the last two on Solana only).
use cabalspy::{CallStatsPeriod, CallTier, CallsOpts, Chain};
let calls = client.calls().list(CallsOpts {
chains: vec![Chain::Solana],
tiers: vec![CallTier::Good, CallTier::Moderate],
..Default::default()
}).await?;
for call in &calls.data {
let peak = call.performance.as_ref().and_then(|p| p.peak_multiple);
println!("{:?} {:?} peak {:?}", call.tier_label, call.token.symbol, peak);
}
let stats = client.calls().stats(Some(CallStatsPeriod::D30), &[], &[]).await?;
println!("{:?} % reached 2x", stats.data.totals.hit_rate.get("2x"));performance.status is "pending" for about two days, until the call has been measured. Older
calls are on the next page, via pagination.next_cursor. For calls as they happen, subscribe to
{"op":"subscribe","stream":"call"} on the websocket gateway.
match client.wallets().tracker(Chain::Solana, addr, None).await {
Ok(env) => println!("{:?}", env.data.period_stats.realized_pnl),
Err(cabalspy::Error::NotFound { .. }) => println!("not tracked"),
Err(cabalspy::Error::InsufficientCredits { .. }) => println!("top up"),
Err(cabalspy::Error::RateLimited { retry_after, .. }) => println!("{retry_after:?}"),
Err(cabalspy::Error::DemoLimit { resets_in_seconds, .. }) => println!("demo resets in {resets_in_seconds:?}s"),
Err(err) => return Err(err),
}Every API error carries .code(), .request_id(), .parameter(), .allowed() and .rate_limit().
429 (except DemoLimit), 5xx and network failures are retried with exponential backoff and jitter; a server-sent
Retry-After wins over the SDK's own timing.
Timestamps inside data arrive as "YYYY-MM-DD HH:MM:SS" with no offset and are UTC.
Only meta.timestamp carries a Z. Treating them as local time shifts every value.
use cabalspy::parse_api_timestamp;
assert_eq!(parse_api_timestamp("2026-07-25 21:33:14"), Some(1785015194)); // Unix seconds, UTCDependency free on purpose. Feed the result into chrono or time if you need a full date type.
It is total_sell - total_buy. A wallet that bought and has not sold reports its whole investment
as a loss, with -100 percent. Check still_holding before showing that number to a user.
Every response struct has an extra: HashMap<String, serde_json::Value> catch-all and every field
is optional, so a server-side addition never breaks deserialization and nothing is silently dropped.
For endpoints not yet modelled, or when you want the raw JSON:
let env = client.get::<serde_json::Value>("/wallets/leaderboard",
vec![("blockchain".into(), "bnb".into()), ("period".into(), "30d".into())]).await?;
println!("{} {:?}", env.status, env.rate_limit.remaining);REST is complete. The websocket gateway is not wrapped yet; until it is, connect to
client.websocket_url() with tokio-tungstenite and send
{"op":"subscribe","stream":"tx","blockchain":"solana","type":"kol"}.
KOL stands for Key Opinion Leader: a trader or crypto personality whose token calls move markets. CabalSpy tracks their onchain wallets with a public identity attached — name, avatar, Twitter and Telegram handle — so you can verify whether they actually bought what they promoted.
A KOL is identified by influence, a smart money wallet by track record. KOL trades carry social signal, smart money trades carry statistical signal. Both are wallet types on the same endpoints, so you can query either or merge them.
On Solana, KOL wallets often buy through Jito bundles together with side wallets they control, which hides the true size of a position. The bundle endpoint groups those wallets, reports a confidence score and exposes the evidence: fee match, block index, adjacency to the KOL transaction.
Tokio. reqwest and the retry backoff both build on it. TLS is rustls, so there is no OpenSSL
dependency and cross compilation stays simple.
docs.cabalspy.xyz · free API key at apidashboard.cabalspy.xyz
- SDK overview on cabalspy.xyz · KOL API · Smart Money API · use cases
- SDKs: TypeScript · Python · Rust
- x402 clients (pay per request, no API key): TypeScript · Python
- MCP server — for Claude, Cursor and VS Code
- CabalSpy Terminal — trade with the same wallet data
MIT