cabalspy 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- cabalspy-0.1.0/.gitignore +11 -0
- cabalspy-0.1.0/LICENSE +21 -0
- cabalspy-0.1.0/PKG-INFO +297 -0
- cabalspy-0.1.0/README.md +261 -0
- cabalspy-0.1.0/cabalspy/__init__.py +169 -0
- cabalspy-0.1.0/cabalspy/_client.py +841 -0
- cabalspy-0.1.0/cabalspy/_constants.py +93 -0
- cabalspy-0.1.0/cabalspy/_errors.py +148 -0
- cabalspy-0.1.0/cabalspy/_realtime.py +327 -0
- cabalspy-0.1.0/cabalspy/_types.py +447 -0
- cabalspy-0.1.0/cabalspy/py.typed +1 -0
- cabalspy-0.1.0/pyproject.toml +51 -0
cabalspy-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 CabalSpy
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
cabalspy-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: cabalspy
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: 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.
|
|
5
|
+
Project-URL: Homepage, https://docs.cabalspy.xyz
|
|
6
|
+
Project-URL: Documentation, https://docs.cabalspy.xyz
|
|
7
|
+
Project-URL: Repository, https://github.com/YOUR_ORG/cabalspy-python
|
|
8
|
+
Project-URL: Issues, https://github.com/YOUR_ORG/cabalspy-python/issues
|
|
9
|
+
Project-URL: Dashboard, https://apidashboard.cabalspy.xyz/
|
|
10
|
+
Author-email: CabalSpy <contact@cabalspy.xyz>
|
|
11
|
+
License-Expression: MIT
|
|
12
|
+
License-File: LICENSE
|
|
13
|
+
Keywords: base,bnb,bsc,bundle-detection,cabalspy,copy-trading,data-api,dex,ethereum,key-opinion-leader,kol,kol-api,kol-tracker,kol-wallets,memecoin,onchain-analytics,onchain-data,pnl,pumpfun,realtime,robinhood-chain,sdk,smart-money,solana,token-api,trading-signals,wallet-tracker,websocket,whale-tracker
|
|
14
|
+
Classifier: Development Status :: 4 - Beta
|
|
15
|
+
Classifier: Intended Audience :: Developers
|
|
16
|
+
Classifier: Intended Audience :: Financial and Insurance Industry
|
|
17
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
18
|
+
Classifier: Programming Language :: Python :: 3
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
23
|
+
Classifier: Topic :: Office/Business :: Financial
|
|
24
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
25
|
+
Classifier: Typing :: Typed
|
|
26
|
+
Requires-Python: >=3.10
|
|
27
|
+
Requires-Dist: httpx>=0.24
|
|
28
|
+
Provides-Extra: dev
|
|
29
|
+
Requires-Dist: mypy>=1.8; extra == 'dev'
|
|
30
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
|
|
31
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
32
|
+
Requires-Dist: websockets>=12.0; extra == 'dev'
|
|
33
|
+
Provides-Extra: realtime
|
|
34
|
+
Requires-Dist: websockets>=12.0; extra == 'realtime'
|
|
35
|
+
Description-Content-Type: text/markdown
|
|
36
|
+
|
|
37
|
+
# CabalSpy Python SDK — KOL and smart money wallet tracking API
|
|
38
|
+
|
|
39
|
+
Official Python client for the **CabalSpy API**: a realtime multichain data layer for labeled wallets, covering **Solana, Base, BNB Chain, Ethereum and Robinhood Chain**.
|
|
40
|
+
|
|
41
|
+
Track what Key Opinion Leaders, smart money wallets and whales are actually buying — with PnL, holder data, cluster signals and bundle detection. Full REST coverage, six WebSocket streams, sync and async.
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
pip install cabalspy
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Realtime needs one extra dependency:
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
pip install "cabalspy[realtime]"
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
**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.
|
|
54
|
+
|
|
55
|
+
## Quick start
|
|
56
|
+
|
|
57
|
+
```python
|
|
58
|
+
from cabalspy import CabalSpy
|
|
59
|
+
|
|
60
|
+
client = CabalSpy() # reads CABALSPY_API_KEY
|
|
61
|
+
|
|
62
|
+
# Who is this wallet? Searches every chain and wallet type.
|
|
63
|
+
wallet = client.wallets.lookup("As7HjL7dzzvbRbaD3WCun47robib2kmAKRXMvjHkSMB5")
|
|
64
|
+
print(wallet["name"], wallet["type"])
|
|
65
|
+
|
|
66
|
+
# Best KOL wallets on Solana over 7 days
|
|
67
|
+
board = client.wallets.leaderboard(blockchain="solana", type="kol", period="7d", limit=25)
|
|
68
|
+
|
|
69
|
+
# Live cluster signals: 5+ KOLs buying the same token within an hour
|
|
70
|
+
signals = client.signals.list(
|
|
71
|
+
blockchain="solana", type="kol", mode="cluster", min_wallets=5, hours=1
|
|
72
|
+
)
|
|
73
|
+
for signal in signals["signals"]:
|
|
74
|
+
print(signal["token"]["token_name"], signal["window"]["wallet_count"])
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## Async
|
|
78
|
+
|
|
79
|
+
Every method exists on `AsyncCabalSpy` with the same signature.
|
|
80
|
+
|
|
81
|
+
```python
|
|
82
|
+
import asyncio
|
|
83
|
+
from cabalspy import AsyncCabalSpy
|
|
84
|
+
|
|
85
|
+
async def main():
|
|
86
|
+
async with AsyncCabalSpy() as client:
|
|
87
|
+
stats = await client.tokens.stats(blockchain="solana", mint="5ti9ASui...")
|
|
88
|
+
print(stats["total_holders"])
|
|
89
|
+
|
|
90
|
+
asyncio.run(main())
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
## Realtime
|
|
94
|
+
|
|
95
|
+
One socket carries every stream. Subscriptions are remembered and re-sent after a reconnect.
|
|
96
|
+
|
|
97
|
+
```python
|
|
98
|
+
import asyncio
|
|
99
|
+
from cabalspy import AsyncCabalSpy, is_tx_position_update
|
|
100
|
+
|
|
101
|
+
async def main():
|
|
102
|
+
async with AsyncCabalSpy() as client:
|
|
103
|
+
rt = client.realtime()
|
|
104
|
+
|
|
105
|
+
@rt.on("position_update")
|
|
106
|
+
def on_trade(msg):
|
|
107
|
+
if is_tx_position_update(msg):
|
|
108
|
+
data = msg["data"]
|
|
109
|
+
print(data["profile"]["name"], data["transaction"]["action"], data["token"]["mint"])
|
|
110
|
+
|
|
111
|
+
@rt.on("kol_bundle")
|
|
112
|
+
def on_bundle(msg):
|
|
113
|
+
print("bundle", msg["data"]["bundle_id"])
|
|
114
|
+
|
|
115
|
+
await rt.connect()
|
|
116
|
+
await rt.subscribe(stream="tx", blockchain="solana", type="kol", token="*")
|
|
117
|
+
await rt.subscribe(stream="bundle", token="MINT_ADDRESS", mode="full", mc_interval=5)
|
|
118
|
+
await rt.subscribe(
|
|
119
|
+
stream="signal",
|
|
120
|
+
blockchain="solana",
|
|
121
|
+
kol={"min_buy": 2, "entry_at": [3, 5, 10], "exit_at": [2, 0]},
|
|
122
|
+
)
|
|
123
|
+
await rt.run_forever()
|
|
124
|
+
|
|
125
|
+
asyncio.run(main())
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
> **`position_update` arrives on two channels with two different payloads.**
|
|
129
|
+
> `tx.<chain>.<type>` sends one wallet and one trade. `holder.<chain>` sends every holder of a
|
|
130
|
+
> token. Use `is_tx_position_update()` and `is_holder_position_update()` to tell them apart.
|
|
131
|
+
|
|
132
|
+
## Chain coverage
|
|
133
|
+
|
|
134
|
+
| Chain | Identifier | Currency | Wallet types |
|
|
135
|
+
|---|---|---|---|
|
|
136
|
+
| Solana | `solana` | SOL | `kol`, `smart`, `whale` |
|
|
137
|
+
| BNB Chain | `bnb` | BNB | `kol`, `smart` |
|
|
138
|
+
| Base | `base` | ETH | `kol`, `smart` |
|
|
139
|
+
| Ethereum | `eth` | ETH | `kol` |
|
|
140
|
+
| Robinhood Chain | `rh` | ETH | `kol`, `smart` |
|
|
141
|
+
|
|
142
|
+
Impossible combinations are rejected before a request goes out, so a typo costs no credits:
|
|
143
|
+
|
|
144
|
+
```python
|
|
145
|
+
client.wallets.list(blockchain="eth", type="whale")
|
|
146
|
+
# BadRequestError: eth supports only: kol (code=invalid_parameter, parameter=type)
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
> **Market cap availability differs between REST and websocket.**
|
|
150
|
+
>
|
|
151
|
+
> On **REST**, `market_cap*`, `price*`, `unrealized_pnl_*` and `remaining_*` are populated for
|
|
152
|
+
> Solana only; on the other chains they come back `None`. Realized PnL, invested amounts, holdings
|
|
153
|
+
> and counters work everywhere.
|
|
154
|
+
>
|
|
155
|
+
> On the **websocket gateway** the same fields are populated on every chain, because the gateway
|
|
156
|
+
> runs its own multichain price service. REST also zeroes `unrealized_pnl_*` once a position is
|
|
157
|
+
> fully sold; the gateway does not.
|
|
158
|
+
|
|
159
|
+
### KOL tracking API for Solana
|
|
160
|
+
|
|
161
|
+
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.
|
|
162
|
+
|
|
163
|
+
```python
|
|
164
|
+
client.bundle.get(mint="MINT_ADDRESS")
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
### KOL tracking API for Base, BNB Chain and Robinhood Chain
|
|
168
|
+
|
|
169
|
+
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.
|
|
170
|
+
|
|
171
|
+
```python
|
|
172
|
+
client.transactions.volume(blockchain="rh", type="kol", hours=24)
|
|
173
|
+
client.wallets.leaderboard(blockchain="bnb", type="kol", period="7d")
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
### KOL tracking API for Ethereum
|
|
177
|
+
|
|
178
|
+
Ethereum mainnet carries `kol` wallets only; smart money signals are unavailable there.
|
|
179
|
+
|
|
180
|
+
## Endpoints
|
|
181
|
+
|
|
182
|
+
**Wallets** — `list`, `history`, `lookup`, `leaderboard`, `tracker`, `holdings`, `pnl_calendar`, `connections`, `batch`
|
|
183
|
+
|
|
184
|
+
**Tokens** — `transactions`, `stats`, `holders`, `batch`
|
|
185
|
+
|
|
186
|
+
**Transactions** — `latest`, `timerange` (max 60 min), `count`, `volume` (max 24 h)
|
|
187
|
+
|
|
188
|
+
**Signals** — `list` (modes `cluster`, `entry`, `exit`), `history` (7, 30, 90 days or `"all"`)
|
|
189
|
+
|
|
190
|
+
**Analytics** — `get` (modes `volume_trend`, `most_traded`, `win_rate`, `top_performers`)
|
|
191
|
+
|
|
192
|
+
**Bundles** — `get` (Solana only)
|
|
193
|
+
|
|
194
|
+
**System** — `health`, `meta`
|
|
195
|
+
|
|
196
|
+
Batch endpoints take at most 100 addresses or mints. `wallets.list` and `wallets.leaderboard` return everything when `limit` is omitted.
|
|
197
|
+
|
|
198
|
+
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.
|
|
199
|
+
|
|
200
|
+
## Errors
|
|
201
|
+
|
|
202
|
+
```python
|
|
203
|
+
from cabalspy import NotFoundError, RateLimitError, InsufficientCreditsError
|
|
204
|
+
|
|
205
|
+
try:
|
|
206
|
+
client.wallets.tracker(blockchain="solana", address=addr)
|
|
207
|
+
except NotFoundError:
|
|
208
|
+
... # wallet is not tracked
|
|
209
|
+
except InsufficientCreditsError:
|
|
210
|
+
... # billing
|
|
211
|
+
except RateLimitError as exc:
|
|
212
|
+
print(exc.retry_after)
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
| Class | Status | Codes |
|
|
216
|
+
|---|---|---|
|
|
217
|
+
| `BadRequestError` | 400 | `missing_parameter`, `invalid_parameter`, `invalid_body` |
|
|
218
|
+
| `AuthenticationError` | 401 | `missing_api_key` |
|
|
219
|
+
| `PermissionError` | 403 | `invalid_api_key` |
|
|
220
|
+
| `InsufficientCreditsError` | 403 | `insufficient_credits` |
|
|
221
|
+
| `NotFoundError` | 404 | `wallet_not_found`, `token_not_found` |
|
|
222
|
+
| `RateLimitError` | 429 | `rate_limit_exceeded` |
|
|
223
|
+
| `ServerError` | 5xx | `internal_error`, `service_unavailable` |
|
|
224
|
+
| `APIConnectionError` | – | network failure, timeout |
|
|
225
|
+
|
|
226
|
+
Every error carries `.code`, `.request_id` and `.docs`; `BadRequestError` also carries `.parameter` and `.allowed`.
|
|
227
|
+
|
|
228
|
+
`429`, `5xx` and network errors are retried automatically with exponential backoff and jitter. A server-sent `Retry-After` wins over the SDK's own backoff.
|
|
229
|
+
|
|
230
|
+
```python
|
|
231
|
+
client = CabalSpy(max_retries=3, timeout=15.0)
|
|
232
|
+
client.wallets.lookup(addr)
|
|
233
|
+
print(client.last_rate_limit) # RateLimit(limit=..., remaining=..., reset=...)
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
## Timestamps
|
|
237
|
+
|
|
238
|
+
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:
|
|
239
|
+
|
|
240
|
+
```python
|
|
241
|
+
from cabalspy import parse_api_date
|
|
242
|
+
|
|
243
|
+
parse_api_date("2026-07-25 21:33:14") # 2026-07-25 21:33:14+00:00, always UTC
|
|
244
|
+
parse_api_date("2026-07-25T21:33:14Z") # ISO works too
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
Only `meta.timestamp` is proper ISO 8601.
|
|
248
|
+
|
|
249
|
+
## A note on `realized_pnl`
|
|
250
|
+
|
|
251
|
+
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.
|
|
252
|
+
|
|
253
|
+
## Pagination
|
|
254
|
+
|
|
255
|
+
`pages()` yields whole envelopes, since the key holding the array differs per endpoint.
|
|
256
|
+
|
|
257
|
+
```python
|
|
258
|
+
for page in client.pages("/wallets/history", {"blockchain": "solana", "address": addr, "limit": 20}):
|
|
259
|
+
print(page.pagination["total"], len(page.data["token_overview"]))
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
`/wallets/history` nests its pagination inside `data` rather than on the envelope; `pages()` handles both, so callers do not have to care.
|
|
263
|
+
|
|
264
|
+
## Configuration
|
|
265
|
+
|
|
266
|
+
```python
|
|
267
|
+
CabalSpy(
|
|
268
|
+
api_key=None, # falls back to CABALSPY_API_KEY
|
|
269
|
+
base_url="https://api.cabalspy.xyz/v1",
|
|
270
|
+
ws_url="wss://stream.cabalspy.xyz",
|
|
271
|
+
timeout=30.0,
|
|
272
|
+
max_retries=2,
|
|
273
|
+
headers={},
|
|
274
|
+
http_client=None, # bring your own httpx.Client
|
|
275
|
+
)
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
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.
|
|
279
|
+
|
|
280
|
+
## Raw access
|
|
281
|
+
|
|
282
|
+
For endpoints not yet wrapped, or when you want the full envelope:
|
|
283
|
+
|
|
284
|
+
```python
|
|
285
|
+
env = client.get_raw("/wallets/leaderboard", {"blockchain": "bnb", "period": "30d"})
|
|
286
|
+
print(env.status, env.meta["cached"], env.rate_limit.remaining, env.pagination)
|
|
287
|
+
|
|
288
|
+
data = client.post_raw("/some/new/endpoint", {"foo": "bar"}).data
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
## Docs and support
|
|
292
|
+
|
|
293
|
+
Full API reference: [docs.cabalspy.xyz](https://docs.cabalspy.xyz) · free API key: [apidashboard.cabalspy.xyz](https://apidashboard.cabalspy.xyz/)
|
|
294
|
+
|
|
295
|
+
## License
|
|
296
|
+
|
|
297
|
+
MIT
|
cabalspy-0.1.0/README.md
ADDED
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
# CabalSpy Python SDK — KOL and smart money wallet tracking API
|
|
2
|
+
|
|
3
|
+
Official Python client for the **CabalSpy API**: a realtime multichain data layer for labeled wallets, covering **Solana, Base, BNB Chain, Ethereum and Robinhood Chain**.
|
|
4
|
+
|
|
5
|
+
Track what Key Opinion Leaders, smart money wallets and whales are actually buying — with PnL, holder data, cluster signals and bundle detection. Full REST coverage, six WebSocket streams, sync and async.
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install cabalspy
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Realtime needs one extra dependency:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
pip install "cabalspy[realtime]"
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
**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.
|
|
18
|
+
|
|
19
|
+
## Quick start
|
|
20
|
+
|
|
21
|
+
```python
|
|
22
|
+
from cabalspy import CabalSpy
|
|
23
|
+
|
|
24
|
+
client = CabalSpy() # reads CABALSPY_API_KEY
|
|
25
|
+
|
|
26
|
+
# Who is this wallet? Searches every chain and wallet type.
|
|
27
|
+
wallet = client.wallets.lookup("As7HjL7dzzvbRbaD3WCun47robib2kmAKRXMvjHkSMB5")
|
|
28
|
+
print(wallet["name"], wallet["type"])
|
|
29
|
+
|
|
30
|
+
# Best KOL wallets on Solana over 7 days
|
|
31
|
+
board = client.wallets.leaderboard(blockchain="solana", type="kol", period="7d", limit=25)
|
|
32
|
+
|
|
33
|
+
# Live cluster signals: 5+ KOLs buying the same token within an hour
|
|
34
|
+
signals = client.signals.list(
|
|
35
|
+
blockchain="solana", type="kol", mode="cluster", min_wallets=5, hours=1
|
|
36
|
+
)
|
|
37
|
+
for signal in signals["signals"]:
|
|
38
|
+
print(signal["token"]["token_name"], signal["window"]["wallet_count"])
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Async
|
|
42
|
+
|
|
43
|
+
Every method exists on `AsyncCabalSpy` with the same signature.
|
|
44
|
+
|
|
45
|
+
```python
|
|
46
|
+
import asyncio
|
|
47
|
+
from cabalspy import AsyncCabalSpy
|
|
48
|
+
|
|
49
|
+
async def main():
|
|
50
|
+
async with AsyncCabalSpy() as client:
|
|
51
|
+
stats = await client.tokens.stats(blockchain="solana", mint="5ti9ASui...")
|
|
52
|
+
print(stats["total_holders"])
|
|
53
|
+
|
|
54
|
+
asyncio.run(main())
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Realtime
|
|
58
|
+
|
|
59
|
+
One socket carries every stream. Subscriptions are remembered and re-sent after a reconnect.
|
|
60
|
+
|
|
61
|
+
```python
|
|
62
|
+
import asyncio
|
|
63
|
+
from cabalspy import AsyncCabalSpy, is_tx_position_update
|
|
64
|
+
|
|
65
|
+
async def main():
|
|
66
|
+
async with AsyncCabalSpy() as client:
|
|
67
|
+
rt = client.realtime()
|
|
68
|
+
|
|
69
|
+
@rt.on("position_update")
|
|
70
|
+
def on_trade(msg):
|
|
71
|
+
if is_tx_position_update(msg):
|
|
72
|
+
data = msg["data"]
|
|
73
|
+
print(data["profile"]["name"], data["transaction"]["action"], data["token"]["mint"])
|
|
74
|
+
|
|
75
|
+
@rt.on("kol_bundle")
|
|
76
|
+
def on_bundle(msg):
|
|
77
|
+
print("bundle", msg["data"]["bundle_id"])
|
|
78
|
+
|
|
79
|
+
await rt.connect()
|
|
80
|
+
await rt.subscribe(stream="tx", blockchain="solana", type="kol", token="*")
|
|
81
|
+
await rt.subscribe(stream="bundle", token="MINT_ADDRESS", mode="full", mc_interval=5)
|
|
82
|
+
await rt.subscribe(
|
|
83
|
+
stream="signal",
|
|
84
|
+
blockchain="solana",
|
|
85
|
+
kol={"min_buy": 2, "entry_at": [3, 5, 10], "exit_at": [2, 0]},
|
|
86
|
+
)
|
|
87
|
+
await rt.run_forever()
|
|
88
|
+
|
|
89
|
+
asyncio.run(main())
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
> **`position_update` arrives on two channels with two different payloads.**
|
|
93
|
+
> `tx.<chain>.<type>` sends one wallet and one trade. `holder.<chain>` sends every holder of a
|
|
94
|
+
> token. Use `is_tx_position_update()` and `is_holder_position_update()` to tell them apart.
|
|
95
|
+
|
|
96
|
+
## Chain coverage
|
|
97
|
+
|
|
98
|
+
| Chain | Identifier | Currency | Wallet types |
|
|
99
|
+
|---|---|---|---|
|
|
100
|
+
| Solana | `solana` | SOL | `kol`, `smart`, `whale` |
|
|
101
|
+
| BNB Chain | `bnb` | BNB | `kol`, `smart` |
|
|
102
|
+
| Base | `base` | ETH | `kol`, `smart` |
|
|
103
|
+
| Ethereum | `eth` | ETH | `kol` |
|
|
104
|
+
| Robinhood Chain | `rh` | ETH | `kol`, `smart` |
|
|
105
|
+
|
|
106
|
+
Impossible combinations are rejected before a request goes out, so a typo costs no credits:
|
|
107
|
+
|
|
108
|
+
```python
|
|
109
|
+
client.wallets.list(blockchain="eth", type="whale")
|
|
110
|
+
# BadRequestError: eth supports only: kol (code=invalid_parameter, parameter=type)
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
> **Market cap availability differs between REST and websocket.**
|
|
114
|
+
>
|
|
115
|
+
> On **REST**, `market_cap*`, `price*`, `unrealized_pnl_*` and `remaining_*` are populated for
|
|
116
|
+
> Solana only; on the other chains they come back `None`. Realized PnL, invested amounts, holdings
|
|
117
|
+
> and counters work everywhere.
|
|
118
|
+
>
|
|
119
|
+
> On the **websocket gateway** the same fields are populated on every chain, because the gateway
|
|
120
|
+
> runs its own multichain price service. REST also zeroes `unrealized_pnl_*` once a position is
|
|
121
|
+
> fully sold; the gateway does not.
|
|
122
|
+
|
|
123
|
+
### KOL tracking API for Solana
|
|
124
|
+
|
|
125
|
+
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.
|
|
126
|
+
|
|
127
|
+
```python
|
|
128
|
+
client.bundle.get(mint="MINT_ADDRESS")
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
### KOL tracking API for Base, BNB Chain and Robinhood Chain
|
|
132
|
+
|
|
133
|
+
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.
|
|
134
|
+
|
|
135
|
+
```python
|
|
136
|
+
client.transactions.volume(blockchain="rh", type="kol", hours=24)
|
|
137
|
+
client.wallets.leaderboard(blockchain="bnb", type="kol", period="7d")
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
### KOL tracking API for Ethereum
|
|
141
|
+
|
|
142
|
+
Ethereum mainnet carries `kol` wallets only; smart money signals are unavailable there.
|
|
143
|
+
|
|
144
|
+
## Endpoints
|
|
145
|
+
|
|
146
|
+
**Wallets** — `list`, `history`, `lookup`, `leaderboard`, `tracker`, `holdings`, `pnl_calendar`, `connections`, `batch`
|
|
147
|
+
|
|
148
|
+
**Tokens** — `transactions`, `stats`, `holders`, `batch`
|
|
149
|
+
|
|
150
|
+
**Transactions** — `latest`, `timerange` (max 60 min), `count`, `volume` (max 24 h)
|
|
151
|
+
|
|
152
|
+
**Signals** — `list` (modes `cluster`, `entry`, `exit`), `history` (7, 30, 90 days or `"all"`)
|
|
153
|
+
|
|
154
|
+
**Analytics** — `get` (modes `volume_trend`, `most_traded`, `win_rate`, `top_performers`)
|
|
155
|
+
|
|
156
|
+
**Bundles** — `get` (Solana only)
|
|
157
|
+
|
|
158
|
+
**System** — `health`, `meta`
|
|
159
|
+
|
|
160
|
+
Batch endpoints take at most 100 addresses or mints. `wallets.list` and `wallets.leaderboard` return everything when `limit` is omitted.
|
|
161
|
+
|
|
162
|
+
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.
|
|
163
|
+
|
|
164
|
+
## Errors
|
|
165
|
+
|
|
166
|
+
```python
|
|
167
|
+
from cabalspy import NotFoundError, RateLimitError, InsufficientCreditsError
|
|
168
|
+
|
|
169
|
+
try:
|
|
170
|
+
client.wallets.tracker(blockchain="solana", address=addr)
|
|
171
|
+
except NotFoundError:
|
|
172
|
+
... # wallet is not tracked
|
|
173
|
+
except InsufficientCreditsError:
|
|
174
|
+
... # billing
|
|
175
|
+
except RateLimitError as exc:
|
|
176
|
+
print(exc.retry_after)
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
| Class | Status | Codes |
|
|
180
|
+
|---|---|---|
|
|
181
|
+
| `BadRequestError` | 400 | `missing_parameter`, `invalid_parameter`, `invalid_body` |
|
|
182
|
+
| `AuthenticationError` | 401 | `missing_api_key` |
|
|
183
|
+
| `PermissionError` | 403 | `invalid_api_key` |
|
|
184
|
+
| `InsufficientCreditsError` | 403 | `insufficient_credits` |
|
|
185
|
+
| `NotFoundError` | 404 | `wallet_not_found`, `token_not_found` |
|
|
186
|
+
| `RateLimitError` | 429 | `rate_limit_exceeded` |
|
|
187
|
+
| `ServerError` | 5xx | `internal_error`, `service_unavailable` |
|
|
188
|
+
| `APIConnectionError` | – | network failure, timeout |
|
|
189
|
+
|
|
190
|
+
Every error carries `.code`, `.request_id` and `.docs`; `BadRequestError` also carries `.parameter` and `.allowed`.
|
|
191
|
+
|
|
192
|
+
`429`, `5xx` and network errors are retried automatically with exponential backoff and jitter. A server-sent `Retry-After` wins over the SDK's own backoff.
|
|
193
|
+
|
|
194
|
+
```python
|
|
195
|
+
client = CabalSpy(max_retries=3, timeout=15.0)
|
|
196
|
+
client.wallets.lookup(addr)
|
|
197
|
+
print(client.last_rate_limit) # RateLimit(limit=..., remaining=..., reset=...)
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
## Timestamps
|
|
201
|
+
|
|
202
|
+
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:
|
|
203
|
+
|
|
204
|
+
```python
|
|
205
|
+
from cabalspy import parse_api_date
|
|
206
|
+
|
|
207
|
+
parse_api_date("2026-07-25 21:33:14") # 2026-07-25 21:33:14+00:00, always UTC
|
|
208
|
+
parse_api_date("2026-07-25T21:33:14Z") # ISO works too
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
Only `meta.timestamp` is proper ISO 8601.
|
|
212
|
+
|
|
213
|
+
## A note on `realized_pnl`
|
|
214
|
+
|
|
215
|
+
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.
|
|
216
|
+
|
|
217
|
+
## Pagination
|
|
218
|
+
|
|
219
|
+
`pages()` yields whole envelopes, since the key holding the array differs per endpoint.
|
|
220
|
+
|
|
221
|
+
```python
|
|
222
|
+
for page in client.pages("/wallets/history", {"blockchain": "solana", "address": addr, "limit": 20}):
|
|
223
|
+
print(page.pagination["total"], len(page.data["token_overview"]))
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
`/wallets/history` nests its pagination inside `data` rather than on the envelope; `pages()` handles both, so callers do not have to care.
|
|
227
|
+
|
|
228
|
+
## Configuration
|
|
229
|
+
|
|
230
|
+
```python
|
|
231
|
+
CabalSpy(
|
|
232
|
+
api_key=None, # falls back to CABALSPY_API_KEY
|
|
233
|
+
base_url="https://api.cabalspy.xyz/v1",
|
|
234
|
+
ws_url="wss://stream.cabalspy.xyz",
|
|
235
|
+
timeout=30.0,
|
|
236
|
+
max_retries=2,
|
|
237
|
+
headers={},
|
|
238
|
+
http_client=None, # bring your own httpx.Client
|
|
239
|
+
)
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
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.
|
|
243
|
+
|
|
244
|
+
## Raw access
|
|
245
|
+
|
|
246
|
+
For endpoints not yet wrapped, or when you want the full envelope:
|
|
247
|
+
|
|
248
|
+
```python
|
|
249
|
+
env = client.get_raw("/wallets/leaderboard", {"blockchain": "bnb", "period": "30d"})
|
|
250
|
+
print(env.status, env.meta["cached"], env.rate_limit.remaining, env.pagination)
|
|
251
|
+
|
|
252
|
+
data = client.post_raw("/some/new/endpoint", {"foo": "bar"}).data
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
## Docs and support
|
|
256
|
+
|
|
257
|
+
Full API reference: [docs.cabalspy.xyz](https://docs.cabalspy.xyz) · free API key: [apidashboard.cabalspy.xyz](https://apidashboard.cabalspy.xyz/)
|
|
258
|
+
|
|
259
|
+
## License
|
|
260
|
+
|
|
261
|
+
MIT
|