npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

cabalspy

v0.2.0

Published

KOL and smart money wallet tracking API for Solana, Base, BNB Chain, Ethereum and Robinhood Chain. Realtime KOL trades, whale wallets, copy-trading signals, bundle detection, token holders and PnL over REST and WebSocket.

Readme

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 and bundle detection. Full v1 REST coverage plus six 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.

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,
});

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));

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] },
});

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.

Bundles

| Method | Endpoint | |---|---| | bundle.get({ mint }) | GET /v1/bundleSolana 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 | | 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?

Rate limits and credits are set per API key plan. See docs.cabalspy.xyz.

Docs

Full API reference: docs.cabalspy.xyz

License

MIT