Skip to main content

CabalSpy Python SDK — KOL and smart money wallet tracking API

Official Python 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. Full REST coverage, seven WebSocket streams, sync and async.

pip install cabalspy

Realtime needs one extra dependency:

pip install "cabalspy[realtime]"

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 on the websocket, with no account:

from cabalspy import CabalSpy

client = CabalSpy.demo()
wallets = client.wallets.list(blockchain="solana", type="kol")
print(client.last_demo["remaining_today"])

Or keyless, straight from a terminal:

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 1)
  • Data is 15 minutes delayed, at most 5 rows per list, no pagination
  • WebSocket: 1 connection per IP, up to 3 subscriptions, closed after 30 minutes

Every demo response carries a demo object (notice, remaining_today, upgrade), exposed as client.last_demo and Envelope.demo. A spent budget raises DemoLimitError with .resets_in_seconds and .upgrade. For real-time data, get a free test key (1,000 requests per month) at apidashboard.cabalspy.xyz, or pay per call with x402 via cabalspy-x402.

Quick start

from cabalspy import CabalSpy

client = CabalSpy()  # reads CABALSPY_API_KEY

# Who is this wallet? Searches every chain and wallet type.
wallet = client.wallets.lookup("As7HjL7dzzvbRbaD3WCun47robib2kmAKRXMvjHkSMB5")
print(wallet["name"], wallet["type"])

# Best KOL wallets on Solana over 7 days
board = client.wallets.leaderboard(blockchain="solana", type="kol", period="7d", limit=25)

# Live cluster signals: 5+ KOLs buying the same token within an hour
signals = client.signals.list(
    blockchain="solana", type="kol", mode="cluster", min_wallets=5, hours=1
)
for signal in signals["signals"]:
    print(signal["token"]["token_name"], signal["window"]["wallet_count"])

# Curated calls (the ones posted in the CabalSpy Discord) and how they performed
calls = client.calls.list(blockchain="solana", tier=["good", "moderate"])
stats = client.calls.stats(period="30d")
print(stats["totals"]["hit_rate"]["2x"], "% of calls reached 2x within 24 h")

Async

Every method exists on AsyncCabalSpy with the same signature.

import asyncio
from cabalspy import AsyncCabalSpy

async def main():
    async with AsyncCabalSpy() as client:
        stats = await client.tokens.stats(blockchain="solana", mint="5ti9ASui...")
        print(stats["total_holders"])

asyncio.run(main())

Realtime

One socket carries every stream. Subscriptions are remembered and re-sent after a reconnect.

import asyncio
from cabalspy import AsyncCabalSpy, is_tx_position_update

async def main():
    async with AsyncCabalSpy() as client:
        rt = client.realtime()

        @rt.on("position_update")
        def on_trade(msg):
            if is_tx_position_update(msg):
                data = msg["data"]
                print(data["profile"]["name"], data["transaction"]["action"], data["token"]["mint"])

        @rt.on("kol_bundle")
        def on_bundle(msg):
            print("bundle", msg["data"]["bundle_id"])

        @rt.on("call")
        def on_call(msg):
            print("call", msg["data"]["tier_label"], msg["data"]["token"]["symbol"])

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

asyncio.run(main())

position_update arrives on two channels with two different payloads. tx.<chain>.<type> sends one wallet and one trade. holder.<chain> sends every holder of a token. Use is_tx_position_update() and is_holder_position_update() to tell them apart.

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

Impossible combinations are rejected before a request goes out, so a typo costs no credits:

client.wallets.list(blockchain="eth", type="whale")
# BadRequestError: eth supports only: kol (code=invalid_parameter, parameter=type)

Market cap availability differs between REST and websocket.

On REST, market_cap*, price*, unrealized_pnl_* and remaining_* are populated for Solana only; on the other chains they come back None. Realized PnL, invested amounts, holdings and counters work everywhere.

On the websocket gateway the same fields are populated on every chain, because the gateway runs its own multichain price service. REST also zeroes unrealized_pnl_* once a position is fully sold; the gateway does not.

KOL tracking API for Solana

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

client.bundle.get(mint="MINT_ADDRESS")

KOL tracking API for Base, BNB Chain and Robinhood Chain

All three carry kol and smart wallets. Robinhood Chain is Robinhood's Ethereum L2 on the Arbitrum Orbit stack, with ETH as the gas token.

client.transactions.volume(blockchain="rh", type="kol", hours=24)
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 there.

Endpoints

Wallets — list, history, lookup, leaderboard, tracker, holdings, pnl_calendar, connections, batch

Tokens — transactions, stats, holders, batch

Transactions — latest, timerange (max 60 min), count, volume (max 24 h)

Signals — list (modes cluster, entry, exit), history (7, 30, 90 days or "all")

Analytics — get (modes volume_trend, most_traded, win_rate, top_performers)

Bundles — get (Solana only)

Calls — list (chains solana, bnb, eth, rh; tiers gamble, moderate, good, big, whale), stats (periods 1d, 7d, 30d, 90d). Walk every page with client.pages("/calls", {...}). performance.status is "pending" for about two days, until the call has been measured over 24 h.

System — health, meta

Batch endpoints take at most 100 addresses or mints. wallets.list and wallets.leaderboard return everything when limit is omitted.

Setting any gated filter on signals.list (kol, smart, kol_min_buy, kol_exit, include_wallets, min_win_rate, min_token_age, …) switches the server into gated mode, which applies an AND gate across wallet types.

Errors

from cabalspy import NotFoundError, RateLimitError, InsufficientCreditsError

try:
    client.wallets.tracker(blockchain="solana", address=addr)
except NotFoundError:
    ...                                  # wallet is not tracked
except InsufficientCreditsError:
    ...                                  # billing
except RateLimitError as exc:
    print(exc.retry_after)
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)
ServerError 5xx internal_error, service_unavailable
APIConnectionError – network failure, timeout

Every error carries .code, .request_id and .docs; BadRequestError also carries .parameter and .allowed.

429 (except demo_limit_reached), 5xx and network errors are retried automatically with exponential backoff and jitter. A server-sent Retry-After wins over the SDK's own backoff.

client = CabalSpy(max_retries=3, timeout=15.0)
client.wallets.lookup(addr)
print(client.last_rate_limit)   # RateLimit(limit=..., remaining=..., reset=...)

Timestamps

Timestamps inside data come back as "YYYY-MM-DD HH:MM:SS" with no timezone. datetime.fromisoformat and friends read those as local time, which silently shifts every value. Use the helper:

from cabalspy import parse_api_date

parse_api_date("2026-07-25 21:33:14")   # 2026-07-25 21:33:14+00:00, always UTC
parse_api_date("2026-07-25T21:33:14Z")  # ISO works too

Only meta.timestamp is proper ISO 8601.

A note on realized_pnl

It is computed as total_sell - total_buy. A wallet that has bought and not yet sold therefore reports its whole investment as a loss, with realized_pnl_percentage: -100. Check still_holding before showing that number to a user.

Pagination

pages() yields whole envelopes, since the key holding the array differs per endpoint.

for page in client.pages("/wallets/history", {"blockchain": "solana", "address": addr, "limit": 20}):
    print(page.pagination["total"], len(page.data["token_overview"]))

/wallets/history nests its pagination inside data rather than on the envelope; pages() handles both, so callers do not have to care.

Configuration

CabalSpy(
    api_key=None,                          # falls back to CABALSPY_API_KEY
    base_url="https://api.cabalspy.xyz/v1",
    ws_url="wss://stream.cabalspy.xyz",
    timeout=30.0,
    max_retries=2,
    headers={},
    http_client=None,                      # bring your own httpx.Client
)

Read the key from the environment or a secret manager, never from source. Use the SDK server side: in a client-side application the key would be readable by anyone.

Raw access

For endpoints not yet wrapped, or when you want the full envelope:

env = client.get_raw("/wallets/leaderboard", {"blockchain": "bnb", "period": "30d"})
print(env.status, env.meta["cached"], env.rate_limit.remaining, env.pagination)

data = client.post_raw("/some/new/endpoint", {"foo": "bar"}).data

Docs and support

Full API reference: docs.cabalspy.xyz · free API key: apidashboard.cabalspy.xyz

License

MIT

Metadata

Release files for cabalspy 0.4.1

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for cabalspy 0.4.1
File Size Uploaded Provenance
cabalspy-0.4.1.tar.gz 24.2 kB With provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for cabalspy 0.4.1
File Interpreter ABI Platform
cabalspy-0.4.1-py3-none-any.whl Python 3 none any Details

Total release size: 51.7 kB

Release files / cabalspy-0.4.1.tar.gz

Download URL cabalspy-0.4.1.tar.gz
Size 24.2 kB
Tags Source
SHA-256 checksum
How to use checksums
d482d75e673271f28a0189d5da9f5580f2c6e0f420b5029fd425215d3f2c7269
BLAKE2b-256 checksum
How to use checksums
9894b9baa73ded188365d8249a32af3aa386314a19ade191ae6df60367c4220c
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 26, 2026.

Transparency log

Release files / cabalspy-0.4.1-py3-none-any.whl

Download URL cabalspy-0.4.1-py3-none-any.whl
Size 27.6 kB
Tags Python 3
SHA-256 checksum
How to use checksums
f17513a1893abcc3b576e58ac289330afaaea2339948d17f60664f95d14ea041
BLAKE2b-256 checksum
How to use checksums
ad9b29ac385c864d2e814074dfbe6b81b5a98093317c86ca771ab456ae945cf8
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 26, 2026.

Transparency log

Security for cabalspy 0.4.1

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

Full provenance

All 2 files in this release have been uploaded with provenance metadata.

Select from 2 files

Attestation for cabalspy-0.4.1.tar.gz

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 26, 2026.

Transparency log

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page