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.
@@ -0,0 +1,11 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ .mypy_cache/
4
+ .pytest_cache/
5
+ dist/
6
+ build/
7
+ *.egg-info/
8
+ .venv/
9
+ venv/
10
+ .env
11
+ .env.*
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.
@@ -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
@@ -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