Skip to content

About

Official TypeScript SDK for the CabalSpy API — KOL, smart money and whale wallet tracking for Solana, Base, BNB Chain, Ethereum and Robinhood Chain

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

10 Commits

Folders and files

Repository files navigation

CabalSpy SDK — KOL and smart money wallet tracking API

Official TypeScript client for the CabalSpy API: a multichain KOL wallet tracking API covering Solana, Base, BNB Chain, Ethereum and Robinhood Chain.

Track what Key Opinion Leaders, smart money wallets and whales are actually buying — in real time, with PnL, holder data, cluster signals, bundle detection and our curated calls. Full v1 REST coverage plus seven WebSocket streams.

npm install cabalspy

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.

Try it without signing up

The public demo key works on every endpoint and the realtime gateway, no account needed:

import { CabalSpy } from 'cabalspy';

const client = CabalSpy.demo();
const kols = await client.wallets.list({ blockchain: 'solana', type: 'kol' });
console.log(client.lastDemo?.remaining_today); // requests left today

Or with nothing but curl:

curl "https://demo-api.cabalspy.xyz/v1/wallets?blockchain=solana&type=kol"
  • 20 requests per IP per UTC day, REST and WebSocket combined (each socket connection counts as one)
  • Data 15 minutes delayed, at most 5 rows per list, no pagination
  • WebSocket: 1 connection per IP, up to 3 subscriptions, closed after 30 minutes
  • A spent budget throws DemoLimitError with .resetsInSeconds and .upgrade links

For real-time data, get a free test key with 1,000 requests a month at apidashboard.cabalspy.xyz, or pay per call with x402 via cabalspy-x402.

Quick start

import { CabalSpy } from 'cabalspy';

const client = new CabalSpy({ apiKey: process.env.CABALSPY_API_KEY });

// Who is this wallet? Searches every chain and wallet type.
const wallet = await client.wallets.lookup('DYw8jCTfwHNRJhhmFcbXvVDTqWMEVFBX6ZKUmG5CNSKK');

// Leaderboard of the best KOL wallets on Solana over 7 days
const board = await client.wallets.leaderboard({
  blockchain: 'solana',
  type: 'kol',
  period: '7d',
  limit: 25,
});

// Live cluster signals: 5+ KOLs buying the same token within an hour
const signals = await client.signals.list({
  blockchain: 'solana',
  type: 'kol',
  mode: 'cluster',
  min_wallets: 5,
  hours: 1,
});

// Curated calls (the ones posted in the CabalSpy Discord) and how they performed
const calls = await client.calls.list({ blockchain: 'solana', tier: ['good', 'moderate'] });
const stats = await client.calls.stats({ period: '30d' });
console.log(stats.totals.hit_rate['2x'], '% of calls reached 2x within 24 h');

Realtime

One socket, all streams. Subscriptions survive reconnects automatically.

import { WebSocket } from 'ws'; // Node < 22 only

const rt = client.realtime({ WebSocketImpl: WebSocket });

rt.on('position_update', (msg) => console.log('trade', msg.data));
rt.on('signal',          (msg) => console.log('signal', msg.data));
rt.on('kol_bundle',      (msg) => console.log('bundle', msg.data));
rt.on('call',            (msg) => console.log('call', msg.data.tier_label, msg.data.token.symbol));

await rt.connect();

rt.subscribe({ stream: 'tx', blockchain: 'solana', type: 'kol', token: '*' });
rt.subscribe({ stream: 'bundle', token: 'MINT_ADDRESS', mode: 'full', mc_interval: 5 });
rt.subscribe({
  stream: 'signal',
  blockchain: 'solana',
  kol: { min_buy: 2, entry_at: [3, 5, 10], exit_at: [2, 0] },
});
rt.subscribe({ stream: 'call', blockchain: 'solana', tiers: ['good'] }); // or omit blockchain for every chain

Chain coverage

Chain Identifier Currency Wallet types
Solana solana SOL kol, smart, whale
BNB Chain bnb BNB kol, smart
Base base ETH kol, smart
Ethereum eth ETH kol
Robinhood Chain rh ETH kol, smart

KOL tracking API for Solana

The deepest coverage of the five. Solana is the only chain with whale wallets, live market cap and unrealized PnL, pump.fun bonding curve progress, and the bundle stream that detects KOL wallets buying through Jito bundles alongside side wallets.

await client.signals.list({ blockchain: 'solana', type: 'kol', mode: 'cluster', min_wallets: 5 });
await client.bundle.get({ mint: 'MINT_ADDRESS' });

KOL tracking API for Base

Base carries both kol and smart wallets. Contract addresses are EVM format, values are denominated in ETH.

await client.transactions.latest({ blockchain: 'base', type: 'smart', limit: 50 });

KOL tracking API for BNB Chain

BNB Chain carries kol and smart wallets, denominated in BNB.

await client.wallets.leaderboard({ blockchain: 'bnb', type: 'kol', period: '7d' });

KOL tracking API for Ethereum

Ethereum mainnet carries kol wallets only. Smart money signals are unavailable on this chain.

await client.wallets.list({ blockchain: 'eth', type: 'kol' });

KOL tracking API for Robinhood Chain

Robinhood Chain is Robinhood's Ethereum L2 on the Arbitrum Orbit stack, live since July 2026, with ETH as the gas token. Both kol and smart wallets are tracked.

await client.transactions.volume({ blockchain: 'rh', type: 'kol', hours: 24 });

The wallet type is bound to the chain at the type level, so invalid combinations fail to compile:

client.wallets.list({ blockchain: 'eth', type: 'whale' });
//                                       ~~~~ Type '"whale"' is not assignable to type '"kol"'

Market cap availability differs between REST and websocket.

On the REST API, market_cap*, price*, unrealized_pnl_* and remaining_* are populated for Solana only. On bnb, base, eth and rh they come back as null. Realized PNL, invested amounts, holdings and counters are available on every chain.

On the websocket gateway, the same fields are populated on every chain, because the gateway runs its own multichain price service. A position_update for a Base wallet carries a real unrealized_pnl_usd, while the equivalent REST call returns null.

One further difference: REST zeroes unrealized_pnl_* once a position is fully sold, the gateway does not — over websocket a closed position keeps reporting the negative of its open cost basis.

REST endpoints

Wallets

Method Endpoint
wallets.list({ blockchain, type, limit?, cursor? }) GET /v1/wallets
wallets.history({ blockchain, address, limit?, cursor? }) GET /v1/wallets/history
wallets.historyPages({ ... }) auto-follows next_cursor
wallets.lookup(address) GET /v1/wallets/lookup
wallets.leaderboard({ blockchain, type?, period?, limit?, cursor? }) GET /v1/wallets/leaderboard
wallets.tracker({ blockchain, address, period? }) GET /v1/wallets/tracker
wallets.holdings({ blockchain, address }) GET /v1/wallets/holdings
wallets.pnlCalendar({ blockchain, address }) GET /v1/wallet/pnl_calendar
wallets.connections({ blockchain, address, limit? }) GET /v1/wallets/connections
wallets.batch({ blockchain, type?, addresses, fields?, period? }) POST /v1/wallets/batch

wallets.list and wallets.leaderboard return everything when limit is omitted. wallets.history defaults to 500 and caps at 1000. Batch requests take at most 100 addresses.

Tokens

Method Endpoint
tokens.transactions({ blockchain, mint, type?, limit? }) GET /v1/tokens/transactions
tokens.stats({ blockchain, mint, type? }) GET /v1/tokens/stats
tokens.holders({ blockchain, mint, type?, limit? }) GET /v1/tokens/holders
tokens.batch({ blockchain, type, mints, fields? }) POST /v1/tokens/batch

Omitting type merges all wallet types of that chain. Batch takes at most 100 mints.

Transactions / feed

Method Endpoint Window
transactions.latest({ blockchain, type, limit?, mint? }) GET /v1/transactions/latest –
transactions.timerange({ ..., minutes }) GET /v1/transactions/timerange max 60 min
transactions.count({ ..., hours }) GET /v1/transactions/count max 24 h
transactions.volume({ ..., hours }) GET /v1/transactions/volume max 24 h

Windows accept seconds, minutes or hours; values above the cap are clamped and reported in warnings.

Signals & analytics

Method Endpoint
signals.list({ blockchain, type, mode, ... }) GET /v1/signals
signals.history({ blockchain, type, days?, mode? }) GET /v1/signals/history
analytics.get({ blockchain, type, mode, period?, limit? }) GET /v1/analytics
  • Signal modes: cluster, entry, exit
  • Analytics modes: volume_trend, most_traded, win_rate, top_performers
  • History windows: 7, 30, 90, all
  • Smart money is unavailable on eth (no smart feed)

Setting any gated filter (kol, smart, kol_min_buy, kol_exit, include_wallets, min_win_rate, min_token_age, …) switches the server into gated mode with an AND gate across types.

Calls

Method Endpoint
calls.list({ blockchain?, tier?, since?, until?, limit?, cursor? }) GET /v1/calls
calls.iterate({ ... }) every page of GET /v1/calls
calls.stats({ period?, blockchain?, tier? }) GET /v1/calls/stats
  • Chains: solana, bnb, eth, rh (no calls on base)
  • Tiers: gamble, moderate, good, big, whale (big and whale on Solana only)
  • performance.status is pending for about two days, until the call has been measured over 24 h

Bundles

Method Endpoint
bundle.get({ mint }) GET /v1/bundle — Solana only

Fully typed response including per-wallet positions and side-wallet proof fields.

System

Method Endpoint
system.health() GET /v1/health
system.meta() GET /v1/meta

Errors

Every failure is a typed subclass of CabalSpyError, so you can branch on the kind instead of parsing strings.

import { NotFoundError, RateLimitError, InsufficientCreditsError } from 'cabalspy';

try {
  await client.wallets.tracker({ blockchain: 'solana', address: addr });
} catch (err) {
  if (err instanceof NotFoundError) return null;              // wallet not tracked
  if (err instanceof InsufficientCreditsError) await topUp(); // billing
  if (err instanceof RateLimitError) console.log(err.retryAfter);
  throw err;
}
Class Status Codes
BadRequestError 400 missing_parameter, invalid_parameter, invalid_body
AuthenticationError 401 missing_api_key
PermissionError 403 invalid_api_key
InsufficientCreditsError 403 insufficient_credits
NotFoundError 404 wallet_not_found, token_not_found
RateLimitError 429 rate_limit_exceeded
DemoLimitError 429 demo_limit_reached (subclass of RateLimitError, not retried)
ServerError 5xx internal_error, service_unavailable
ConnectionError – network failure, timeout

BadRequestError carries .parameter and .allowed, so validation failures are actionable. Every error carries .requestId and .docs.

Retries and rate limits

429 and 5xx responses plus network errors are retried automatically with exponential backoff and jitter. When the server sends Retry-After, that value wins over the SDK's own backoff.

const client = new CabalSpy({ apiKey, maxRetries: 3, timeout: 15_000 });

await client.wallets.lookup(addr);
console.log(client.lastRateLimit); // { limit, remaining, reset }

Each authenticated request costs 10 credits. Rate limits are per minute and per plan.

Pagination

Cursor-paginated endpoints expose a page iterator. It yields whole envelopes, since the item key differs per endpoint:

for await (const page of client.wallets.historyPages({ blockchain: 'solana', address: addr })) {
  console.log(page.pagination?.total, page.data);
}

Configuration

new CabalSpy({
  apiKey: process.env.CABALSPY_API_KEY, // required (env fallback: CABALSPY_API_KEY)
  baseUrl: 'https://api.cabalspy.xyz/v1',
  wsUrl: 'wss://stream.cabalspy.xyz',
  timeout: 30_000,
  maxRetries: 2,
  headers: {},
  fetch: undefined,                     // custom fetch (proxies, tests)
});

Never hardcode the key. Read it from the environment or a secret manager.

Raw access

Every resource method has a *Raw variant returning the full envelope (data, pagination, meta, rateLimit, status). For endpoints not yet wrapped, call the client directly:

const env = await client.wallets.leaderboardRaw({ blockchain: 'bnb', period: '30d' });
console.log(env.meta.cached, env.meta.cache_age_seconds);

const custom = await client.get('/some/new/endpoint', { blockchain: 'solana' });

FAQ

What is a KOL wallet?

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.

How is smart money different from a KOL?

A KOL is identified by influence, a smart money wallet by track record. KOL trades carry social signal; smart money trades carry statistical signal. CabalSpy exposes both as separate wallet types on the same endpoints, so you can query either or merge them.

What does bundle detection do?

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 and stream group those wallets, report a confidence score and expose the evidence — fee match, block index, adjacency to the KOL transaction.

Does it work in the browser?

The SDK runs anywhere fetch exists, but your API key would be readable in a browser bundle. Use it server side and proxy requests from your frontend.

Is there a free tier?

Yes. CabalSpy.demo() needs no account (20 requests a day, 15 minutes delayed), and a free test key at apidashboard.cabalspy.xyz gives 1,000 real-time requests a month. Paid limits and credits are per plan, see docs.cabalspy.xyz.

Docs

Full API reference: docs.cabalspy.xyz

Related

License

MIT

About

Official TypeScript SDK for the CabalSpy API — KOL, smart money and whale wallet tracking for Solana, Base, BNB Chain, Ethereum and Robinhood Chain

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages