cabalspy-id 0.5.1__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.
Files changed (38) hide show
  1. cabalspy_id-0.5.1/.gitignore +12 -0
  2. cabalspy_id-0.5.1/CHANGELOG.md +105 -0
  3. cabalspy_id-0.5.1/LICENSE +21 -0
  4. cabalspy_id-0.5.1/PKG-INFO +495 -0
  5. cabalspy_id-0.5.1/README.md +449 -0
  6. cabalspy_id-0.5.1/cabalspy_id/__init__.py +271 -0
  7. cabalspy_id-0.5.1/cabalspy_id/_client.py +959 -0
  8. cabalspy_id-0.5.1/cabalspy_id/_constants.py +68 -0
  9. cabalspy_id-0.5.1/cabalspy_id/_errors.py +189 -0
  10. cabalspy_id-0.5.1/cabalspy_id/_oidc.py +599 -0
  11. cabalspy_id-0.5.1/cabalspy_id/_stream.py +431 -0
  12. cabalspy_id-0.5.1/cabalspy_id/_theses_stream.py +444 -0
  13. cabalspy_id-0.5.1/cabalspy_id/_thesis.py +717 -0
  14. cabalspy_id-0.5.1/cabalspy_id/_types.py +710 -0
  15. cabalspy_id-0.5.1/cabalspy_id/_users.py +46 -0
  16. cabalspy_id-0.5.1/cabalspy_id/_webhooks.py +116 -0
  17. cabalspy_id-0.5.1/cabalspy_id/api.py +201 -0
  18. cabalspy_id-0.5.1/cabalspy_id/py.typed +0 -0
  19. cabalspy_id-0.5.1/cabalspy_id/thesis.py +146 -0
  20. cabalspy_id-0.5.1/examples/data_api.py +43 -0
  21. cabalspy_id-0.5.1/examples/login_server.py +93 -0
  22. cabalspy_id-0.5.1/examples/stream.py +42 -0
  23. cabalspy_id-0.5.1/examples/theses.py +39 -0
  24. cabalspy_id-0.5.1/examples/webhook_receiver.py +38 -0
  25. cabalspy_id-0.5.1/pyproject.toml +115 -0
  26. cabalspy_id-0.5.1/tests/__init__.py +0 -0
  27. cabalspy_id-0.5.1/tests/conftest.py +116 -0
  28. cabalspy_id-0.5.1/tests/live/__init__.py +0 -0
  29. cabalspy_id-0.5.1/tests/live/test_live.py +146 -0
  30. cabalspy_id-0.5.1/tests/test_client.py +255 -0
  31. cabalspy_id-0.5.1/tests/test_notifications.py +137 -0
  32. cabalspy_id-0.5.1/tests/test_oidc.py +257 -0
  33. cabalspy_id-0.5.1/tests/test_stream.py +180 -0
  34. cabalspy_id-0.5.1/tests/test_submodules.py +32 -0
  35. cabalspy_id-0.5.1/tests/test_theses.py +447 -0
  36. cabalspy_id-0.5.1/tests/test_theses_stream.py +219 -0
  37. cabalspy_id-0.5.1/tests/test_users.py +50 -0
  38. cabalspy_id-0.5.1/tests/test_webhooks.py +78 -0
@@ -0,0 +1,12 @@
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.*
12
+ .ruff_cache/
@@ -0,0 +1,105 @@
1
+ # Changelog
2
+
3
+ All notable changes to this package are documented here. The format follows
4
+ [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and the project uses
5
+ [Semantic Versioning](https://semver.org/).
6
+
7
+ ## [0.5.1] - 2026-10-15
8
+
9
+ ### Changed
10
+
11
+ - People can sign in with X, Telegram or Google. Types match accounts without X: `x` is
12
+ `None` in userinfo claims and Data API profiles, `twitter` is `None` in profiles, trading
13
+ profile blocks and leaderboards, `telegram` is documented (`https://t.me/<name>` or `""`),
14
+ handles may be `tg.<name>` or `id.<hex>`.
15
+
16
+ ### Added
17
+
18
+ - `has_x()` and `handle_kind()` helpers and a README section "Users without X".
19
+
20
+ ## [0.5.0] - 2026-10-14
21
+
22
+ ### Added
23
+
24
+ - Modules `cabalspy_id.api` (Data API, trade stream, login, webhooks) and `cabalspy_id.thesis`
25
+ (thesis methods, `ThesisClient`, `ThesesStream`), exposing the same objects as `cabalspy_id`.
26
+ - Packages table (socialfi-api, socialfi-thesis, socialfi-widget) in the README.
27
+
28
+ ## [0.4.1] - 2026-10-14
29
+
30
+ ### Changed
31
+
32
+ - Package description, keywords, classifiers and README: CabalSpy ID as a SocialFi API & SDK. No code changes.
33
+
34
+ ## [0.4.0] - 2026-10-14
35
+
36
+ ### Added
37
+
38
+ - `Thesis["reacted_by"]` (who reacted recently) and `Thesis["latest_replies"]` (reply preview).
39
+ - Moderation notifications: Data API `thesis_notifications(csid_id)` and
40
+ `appeal_thesis_decision(csid_id, notification_id, message)`; public `ThesisClient.notifications()`,
41
+ `mark_notifications_read()` and `appeal(notification_id, message)`. `TypedDict`s
42
+ `ThesisNotification`, `ThesisNotifications`, `ThesisNotices`; `me()` returns `notices`.
43
+ - Webhook types `thesis.reply` (`data["reply"]`) and `thesis.reaction`
44
+ (`thesis_id`, `chain`, `mint`, `emoji`, `on`, `reactions`).
45
+ - Error code `reporting_paused` (403, `PermissionError`) documented.
46
+
47
+ ## [0.3.0] - 2026-10-13
48
+
49
+ ### Added
50
+
51
+ - `scope` (`global` | `instance`) on `list_theses`, `iter_theses`, `thesis_summary` and
52
+ `latest_theses`.
53
+ - `ThesesStream` (`client.theses_stream()`, async) for `wss://api-id.cabalspy.xyz/v1/theses/stream`
54
+ (every plan): `subscribe` / `unsubscribe` up to 200 tokens, `subscribe_all` / `unsubscribe_all`,
55
+ subscriptions restored after reconnects, heartbeat watchdog, handlers and async iteration.
56
+ - `ThesisClient` / `AsyncThesisClient`: `scope` and `client_id` (reads, `live()`, post attribution).
57
+
58
+ ## [0.2.0] - 2026-10-13
59
+
60
+ ### Added
61
+
62
+ - Token theses in the Data API (sync and async): `list_theses` / `iter_theses`, `thesis_summary`,
63
+ `thesis`, `thesis_replies` / `iter_thesis_replies`, `latest_theses`, `user_theses`,
64
+ `thesis_eligibility`, and writes in a user's name: `post_thesis`, `reply_to_thesis`,
65
+ `react_to_thesis`, `report_thesis`, `delete_thesis`. `TypedDict`s `Thesis`, `ThesisPosition`,
66
+ `ThesisSummary`, `ThesisEligibility`, `ThesisReactions`.
67
+ - `post_raw`. Writes are only retried after the API's own rate limit (`rate_limit_exceeded`),
68
+ never after a server or network error.
69
+ - Stream: `events=["trades", "theses"]` and `mint=`; events `thesis`, `thesis.reply`,
70
+ `thesis.reaction`, `thesis.removed`; `stream.theses()` iterator.
71
+ - `ThesisClient` / `AsyncThesisClient` for the public thesis API
72
+ (`https://id.cabalspy.xyz/thesis/v1`) with a user's access token, and `ThesisLive` (`.live()`)
73
+ for the public per-token websocket.
74
+ - Webhook type `thesis.created`, OAuth scope `thesis`.
75
+
76
+ ## [0.1.0] - 2026-10-13
77
+
78
+ First release.
79
+
80
+ ### Added
81
+
82
+ - `CabalSpyID` and `AsyncCabalSpyID` Data API clients for `https://api-id.cabalspy.xyz/v1`:
83
+ `health`, `me`, `users`, `profile`, `wallet_lookup`, `tracker`, `history`, `holdings`,
84
+ `pnl_calendar`, `leaderboard`, `latest_transactions`, with `instance` support, retries with
85
+ backoff, rate limit info, `get_raw` / `pages` and iterators (`iter_users`, `iter_history`,
86
+ `iter_leaderboard`).
87
+ - `CabalSpyIDStream` (async, `[stream]` extra) for the live trade stream (`/v1/stream`):
88
+ auto-reconnect, heartbeat watchdog, fatal configuration errors, async iteration.
89
+ - `CabalSpyIDAuth` and `AsyncCabalSpyIDAuth` OpenID Connect helpers: discovery,
90
+ `create_authorization_url` (PKCE S256, state, nonce), `handle_callback`, `exchange_code`,
91
+ `refresh`, `revoke`, `userinfo`, `verify_id_token` and `verify_access_token` (ES256 via JWKS,
92
+ PyJWT), `is_limited`, `generate_pkce`.
93
+ - `verify_webhook` / `sign_webhook` for Standard Webhooks signatures.
94
+ - `TypedDict` response types and `py.typed`.
95
+ - Exceptions: `BadRequestError`, `AuthenticationError`, `PermissionError`, `PlanUserLimitError`,
96
+ `NotFoundError`, `RateLimitError`, `ServerError`, `APIConnectionError`,
97
+ `InvalidResponseError`, `OAuthError`, `TokenVerificationError`, `WebhookVerificationError`.
98
+
99
+ [0.5.1]: https://github.com/CabalSpy/cabalspy-id-python/compare/v0.5.0...v0.5.1
100
+ [0.5.0]: https://github.com/CabalSpy/cabalspy-id-python/compare/v0.4.1...v0.5.0
101
+ [0.4.1]: https://github.com/CabalSpy/cabalspy-id-python/compare/v0.4.0...v0.4.1
102
+ [0.4.0]: https://github.com/CabalSpy/cabalspy-id-python/compare/v0.3.0...v0.4.0
103
+ [0.3.0]: https://github.com/CabalSpy/cabalspy-id-python/compare/v0.2.0...v0.3.0
104
+ [0.2.0]: https://github.com/CabalSpy/cabalspy-id-python/compare/v0.1.0...v0.2.0
105
+ [0.1.0]: https://github.com/CabalSpy/cabalspy-id-python/releases/tag/v0.1.0
@@ -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,495 @@
1
+ Metadata-Version: 2.5
2
+ Name: cabalspy-id
3
+ Version: 0.5.1
4
+ Summary: SocialFi API & SDK for CabalSpy ID — add a social layer to any app: login with X + verified wallets, live trades and PnL of your users, token theses, leaderboards and copy trading on Solana, BNB Chain, Base, Ethereum and Robinhood Chain.
5
+ Project-URL: Homepage, https://id.cabalspy.xyz
6
+ Project-URL: Website, https://www.cabalspy.xyz/cabalspy-id/
7
+ Project-URL: Documentation, https://docs.cabalspy.xyz/cabalspy-id
8
+ Project-URL: Repository, https://github.com/CabalSpy/cabalspy-id-python
9
+ Project-URL: Issues, https://github.com/CabalSpy/cabalspy-id-python/issues
10
+ Project-URL: Changelog, https://github.com/CabalSpy/cabalspy-id-python/blob/main/CHANGELOG.md
11
+ Project-URL: Dashboard, https://apidashboard.cabalspy.xyz/
12
+ Author-email: CabalSpy <contact@cabalspy.xyz>
13
+ License-Expression: MIT
14
+ License-File: LICENSE
15
+ Keywords: base,bnb,cabalspy,copy-trading,crypto-login,crypto-social,ethereum,kol,kol-tracker,oidc,openid-connect,pnl,python,sdk,social-trading,socialfi,socialfi-api,socialfi-sdk,socialfi-widget,solana,token-thesis,trading-leaderboard,wallet-tracker,web3-social
16
+ Classifier: Development Status :: 4 - Beta
17
+ Classifier: Framework :: AsyncIO
18
+ Classifier: Intended Audience :: Developers
19
+ Classifier: Intended Audience :: Financial and Insurance Industry
20
+ Classifier: License :: OSI Approved :: MIT License
21
+ Classifier: Programming Language :: Python :: 3
22
+ Classifier: Programming Language :: Python :: 3.10
23
+ Classifier: Programming Language :: Python :: 3.11
24
+ Classifier: Programming Language :: Python :: 3.12
25
+ Classifier: Programming Language :: Python :: 3.13
26
+ Classifier: Topic :: Communications
27
+ Classifier: Topic :: Internet :: WWW/HTTP
28
+ Classifier: Topic :: Internet :: WWW/HTTP :: Session
29
+ Classifier: Topic :: Office/Business :: Financial
30
+ Classifier: Topic :: Office/Business :: Financial :: Investment
31
+ Classifier: Topic :: Security
32
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
33
+ Classifier: Typing :: Typed
34
+ Requires-Python: >=3.10
35
+ Requires-Dist: httpx>=0.24
36
+ Requires-Dist: pyjwt[crypto]>=2.8
37
+ Provides-Extra: dev
38
+ Requires-Dist: mypy>=1.8; extra == 'dev'
39
+ Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
40
+ Requires-Dist: pytest>=8.0; extra == 'dev'
41
+ Requires-Dist: ruff>=0.6; extra == 'dev'
42
+ Requires-Dist: websockets>=14.0; extra == 'dev'
43
+ Provides-Extra: stream
44
+ Requires-Dist: websockets>=14.0; extra == 'stream'
45
+ Description-Content-Type: text/markdown
46
+
47
+ # CabalSpy ID — SocialFi API & SDK for Python
48
+
49
+ [![PyPI](https://img.shields.io/pypi/v/cabalspy-id.svg)](https://pypi.org/project/cabalspy-id/) [![ci](https://github.com/CabalSpy/cabalspy-id-python/actions/workflows/ci.yml/badge.svg)](https://github.com/CabalSpy/cabalspy-id-python/actions/workflows/ci.yml) [![Python](https://img.shields.io/pypi/pyversions/cabalspy-id.svg)](https://pypi.org/project/cabalspy-id/) [![license](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
50
+
51
+ Official Python SDK (sync and async) for [CabalSpy ID](https://id.cabalspy.xyz). **CabalSpy ID is the SocialFi API for crypto apps**: one SocialFi integration gives your platform login with X, wallets verified by a tiny on-chain payment (no wallet connect, no signatures), reviewed KOL badges, live trades and PnL of your users and a drop-in SocialFi widget for token theses — on **Solana, BNB Chain, Base, Ethereum and Robinhood Chain**.
52
+
53
+ Your platform adds "Login with CabalSpy ID" and receives the user's X profile, verified wallets, KOL badge and full trading stats — PnL, win rate, trades, holdings — for copy trading, leaderboards and social features.
54
+
55
+ [Website](https://www.cabalspy.xyz/cabalspy-id/) · [Docs](https://docs.cabalspy.xyz/cabalspy-id) · [Sign in](https://id.cabalspy.xyz) · [API dashboard](https://apidashboard.cabalspy.xyz)
56
+
57
+ ```bash
58
+ pip install cabalspy-id
59
+ ```
60
+
61
+ The live stream needs one extra dependency:
62
+
63
+ ```bash
64
+ pip install "cabalspy-id[stream]"
65
+ ```
66
+
67
+ | | |
68
+ |---|---|
69
+ | `CabalSpyIDAuth` / `AsyncCabalSpyIDAuth` | OpenID Connect for your server: PKCE, code exchange, refresh, revoke, userinfo, ID token verification (ES256 / JWKS) |
70
+ | `CabalSpyID` / `AsyncCabalSpyID` | Data API client for `https://api-id.cabalspy.xyz/v1`, typed responses, pagination, live trade stream |
71
+ | `ThesisClient` / `AsyncThesisClient` | public token thesis API with a user's access token, plus `live()` updates |
72
+ | `verify_webhook` | Standard Webhooks signature verification |
73
+
74
+ Sync and async, fully typed (`py.typed`, `TypedDict` responses). Python 3.10+.
75
+
76
+ Everything for developers — instance (client_id), client secrets, redirect URIs, button builder, webhooks, API keys — lives in the [API dashboard](https://apidashboard.cabalspy.xyz), tab **CabalSpy ID**. For React and the browser, see the [TypeScript SDK](https://www.npmjs.com/package/cabalspy-id).
77
+
78
+ ## What can you build
79
+
80
+ - **Social trading feed** — every trade of your users live, with their X profile and PnL
81
+ - **Leaderboards** — rank your users by PnL, volume, win rate or trades
82
+ - **Copy trading** — follow verified wallets of real, identified traders
83
+ - **Token theses** — holders-only opinions on a token with the author's real position, as a SocialFi widget or your own UI
84
+ - **Trader profiles** — X profile, verified wallets, holdings, PnL calendar
85
+ - **KOL badges** — reviewed badges for influential traders, shown wherever they sign in
86
+
87
+ ## Packages
88
+
89
+ One codebase, one version line. The focused packages re-export parts of `cabalspy-id`, so they always behave exactly the same.
90
+
91
+ | Package | npm | PyPI | What it contains |
92
+ |---|---|---|---|
93
+ | **cabalspy-id** | [`cabalspy-id`](https://www.npmjs.com/package/cabalspy-id) | [`cabalspy-id`](https://pypi.org/project/cabalspy-id/) | everything below |
94
+ | **socialfi-api** ([repo](https://github.com/CabalSpy/socialfi-api)) | [`socialfi-api`](https://www.npmjs.com/package/socialfi-api) | [`socialfi-api`](https://pypi.org/project/socialfi-api/) | the SocialFi API: Data API, Login with CabalSpy ID, trade stream, webhooks (= `cabalspy-id/api`, `cabalspy_id.api`) |
95
+ | **socialfi-thesis** ([repo](https://github.com/CabalSpy/socialfi-thesis)) | [`socialfi-thesis`](https://www.npmjs.com/package/socialfi-thesis) | [`socialfi-thesis`](https://pypi.org/project/socialfi-thesis/) | token theses: Data API thesis methods, public thesis client, theses stream (= `cabalspy-id/thesis`, `cabalspy_id.thesis`) |
96
+ | **socialfi-widget** ([repo](https://github.com/CabalSpy/socialfi-widget)) | [`socialfi-widget`](https://www.npmjs.com/package/socialfi-widget) | – | the browser SocialFi widgets: login button and theses widget, vanilla and React (= `cabalspy-id/widget`) |
97
+
98
+ ### Modules
99
+
100
+ | Import | Contents |
101
+ |---|---|
102
+ | `cabalspy_id` | everything |
103
+ | `cabalspy_id.api` | `CabalSpyID`, `AsyncCabalSpyID`, `CabalSpyIDStream`, `CabalSpyIDAuth`, `verify_webhook`, errors and Data API types |
104
+ | `cabalspy_id.thesis` | `CabalSpyID` (thesis methods), `ThesisClient`, `AsyncThesisClient`, `ThesisLive`, `ThesesStream`, thesis types |
105
+
106
+ The objects are the same as in `cabalspy_id`.
107
+
108
+ ## Login button
109
+
110
+ Put the official button on your page and point it at your login route:
111
+
112
+ ```html
113
+ <div data-csid-login data-client-id="csid_..." data-href="/login"></div>
114
+ <script src="https://id.cabalspy.xyz/widget.js" async></script>
115
+ ```
116
+
117
+ Signed in, mark the person so the button shows their avatar menu: `data-user="<sub>" data-logout-href="/logout"`. Design overrides: `data-theme`, `data-color`, `data-text-color`, `data-shape`, `data-size`, `data-label`, `data-full-width`, `data-show-logo`.
118
+
119
+ ## Server-side login (OpenID Connect)
120
+
121
+ Authorization code flow with PKCE S256 (required for every client), state and nonce.
122
+
123
+ ```python
124
+ from cabalspy_id import CabalSpyIDAuth, is_limited
125
+
126
+ auth = CabalSpyIDAuth(
127
+ "csid_...", # your instance's client_id
128
+ client_secret=os.environ["CSID_CLIENT_SECRET"], # csid_sk_... (omit for public clients)
129
+ redirect_uri="https://your.app/callback", # registered in the dashboard
130
+ )
131
+
132
+ # GET /login
133
+ req = auth.create_authorization_url(scope=["openid", "profile", "wallets", "offline_access"])
134
+ session["csid"] = req.to_dict() # state, nonce, code_verifier, redirect_uri
135
+ return redirect(req.url)
136
+
137
+ # GET /callback
138
+ result = auth.handle_callback(request.url, **session.pop("csid"))
139
+ # checks error, state and iss, exchanges the code, verifies the ID token and its nonce
140
+
141
+ if is_limited(result.claims):
142
+ ... # over your plan's user limit: anonymous sub "anon_...", no profile or wallet data
143
+ else:
144
+ user = auth.userinfo(result.tokens["access_token"]) # x profile, wallets, onchain
145
+ ```
146
+
147
+ | Method | |
148
+ |---|---|
149
+ | `discover()` | the discovery document (cached) |
150
+ | `create_authorization_url(scope=, prompt=, redirect_uri=, state=, nonce=)` | returns `AuthorizationRequest(url, state, nonce, code_verifier, redirect_uri)` |
151
+ | `handle_callback(url, state=, nonce=, code_verifier=)` | full callback handling, returns `LoginResult(tokens, claims)` |
152
+ | `exchange_code(code=, code_verifier=, redirect_uri=)` | `POST /oauth/token`, authorization_code |
153
+ | `refresh(refresh_token)` | `POST /oauth/token`, refresh_token. Tokens rotate: store the new one; reusing an old one revokes the chain |
154
+ | `revoke(token)` | `POST /oauth/revoke` |
155
+ | `userinfo(access_token)` | `GET /oauth/userinfo` |
156
+ | `verify_id_token(id_token, nonce=, max_age=)` | ES256 signature via JWKS, iss, aud, azp, exp, iat, nonce |
157
+ | `verify_access_token(access_token)` | for access tokens your frontend sends to your API (`typ: at+jwt`) |
158
+
159
+ `AsyncCabalSpyIDAuth` has the same methods as coroutines. Scopes: `openid` `profile` `wallets` `onchain` `thesis` `offline_access` (`thesis`: post theses and replies, react and report in the user's name). Client authentication: `client_secret_basic` (default with a secret), `client_secret_post`, `none`. Access and ID tokens live 15 minutes; refresh tokens 30 days of inactivity, 90 days absolute.
160
+
161
+ **Browser apps** (public clients) let the widget run PKCE itself; it hands your page an `idToken`. Send it to your backend and verify it there:
162
+
163
+ ```python
164
+ auth = CabalSpyIDAuth("csid_...")
165
+ claims = auth.verify_id_token(id_token_from_the_browser)
166
+ ```
167
+
168
+ **Users over the plan's limit can always sign in.** Their token carries an anonymous `sub` (`anon_...`), `limited: true` and `reason: "plan_user_limit"`, and no profile or wallet data, until the platform upgrades. `is_limited(claims)` checks both.
169
+
170
+ A runnable server is in [examples/login_server.py](examples/login_server.py).
171
+
172
+ ## Users without X
173
+
174
+ People can sign in to CabalSpy ID with **X, Telegram or Google**. Accounts without X can do everything except earn the KOL badge, and connecting X later fills in their profile. For them:
175
+
176
+ | Field | With X | Without X |
177
+ |---|---|---|
178
+ | `preferred_username` (claims) / `username` | X handle, e.g. `alice` | `tg.<telegram name>` (Telegram) or `id.<hex>` (Google) — always contains a dot |
179
+ | `x` (userinfo, `/v1/profile`) | X profile / stats | `None` |
180
+ | `twitter` (profiles, trading profile blocks, leaderboards) | `https://x.com/<handle>` | `None` |
181
+ | `telegram` (profiles, trading profile blocks, leaderboards) | `https://t.me/<name>` or `""` | `https://t.me/<name>` or `""` |
182
+ | `name`, `picture` / `image_url` | from X | from Telegram or Google |
183
+
184
+ `profile_url` (`https://id.cabalspy.xyz/u/<handle>`) works for everyone. Thesis authors use the same handles.
185
+
186
+ ```python
187
+ from cabalspy_id import has_x, handle_kind
188
+
189
+ has_x(claims) # False for Telegram/Google sign-ins
190
+ handle_kind("tg.alice") # "telegram"
191
+ x_link = profile["twitter"] # None without X: never build x.com links from tg./id. handles
192
+ ```
193
+
194
+ ## Data API
195
+
196
+ ```python
197
+ from cabalspy_id import CabalSpyID
198
+
199
+ csid = CabalSpyID() # reads CABALSPY_ID_API_KEY, then CABALSPY_API_KEY
200
+
201
+ me = csid.me() # instance, plan, rate limit, users with data
202
+
203
+ # The person who just signed in: look them up by the `sub` of their ID token
204
+ profile = csid.profile(csid_id=claims["sub"])
205
+ wallet = next(w for w in profile["wallets"] if "solana" in w["chains"])
206
+
207
+ # Full trading profile for a period, same numbers as the CabalSpy API
208
+ t = csid.tracker(blockchain="solana", address=wallet["address"], period="7d")
209
+ print(t["period_stats"]["realized_pnl_usd"], t["period_win_rate_distribution"]["win_rate_percentage"])
210
+
211
+ # Your users ranked by PnL
212
+ board = csid.leaderboard(blockchain="solana", period="7d", sort="pnl_usd", limit=25)
213
+ ```
214
+
215
+ Async:
216
+
217
+ ```python
218
+ from cabalspy_id import AsyncCabalSpyID
219
+
220
+ async with AsyncCabalSpyID() as csid:
221
+ users = await csid.users(kol=True)
222
+ ```
223
+
224
+ The API key is the **same key as the CabalSpy API**, with the per-key toggle "CabalSpy ID" enabled on the API keys page. A key sees **its instance only**: users that signed in to its apps, within the plan's user limit. `scope="global"` (every CabalSpy ID user) is Enterprise only.
225
+
226
+ | Method | Endpoint |
227
+ |---|---|
228
+ | `health()` | `GET /v1/health` (no key needed) |
229
+ | `me()` | `GET /v1/me` |
230
+ | `users(limit=, kol=, cursor=, scope=)` | `GET /v1/users` |
231
+ | `profile(username= \| csid_id= \| address=)` | `GET /v1/profile` |
232
+ | `wallet_lookup(address)` | `GET /v1/wallets/lookup` |
233
+ | `tracker(blockchain=, address=, period=)` | `GET /v1/wallets/tracker` |
234
+ | `history(blockchain=, address=, limit=, cursor=)` | `GET /v1/wallets/history` |
235
+ | `holdings(blockchain=, address=)` | `GET /v1/wallets/holdings` |
236
+ | `pnl_calendar(blockchain=, address=)` | `GET /v1/wallets/pnl_calendar` |
237
+ | `leaderboard(blockchain=, period=, sort=, limit=, cursor=, scope=)` | `GET /v1/wallets/leaderboard` |
238
+ | `latest_transactions(blockchain=, limit=)` | `GET /v1/transactions/latest` |
239
+
240
+ Chains: `solana`, `bnb`, `base`, `eth`, `rh`. Periods: `6h`, `1d`, `7d`, `30d`. Leaderboard sorts: `volume`, `pnl`, `pnl_usd`, `win_rate`, `trades`. Responses are plain dicts typed with `TypedDict`s (`Profile`, `TrackerResponse`, `LeaderboardResponse`, …).
241
+
242
+ If your account has several CabalSpy ID instances, pass `instance="csid_..."` (the client_id).
243
+
244
+ ### Pagination
245
+
246
+ Iterators follow `next_cursor` for you (async generators on `AsyncCabalSpyID`):
247
+
248
+ ```python
249
+ for user in csid.iter_users(kol=True):
250
+ print(user["username"])
251
+ for trade in csid.iter_history(blockchain="solana", address=address):
252
+ print(trade["tx_signature"])
253
+ for entry in csid.iter_leaderboard(blockchain="base", period="30d"):
254
+ print(entry["rank"])
255
+ ```
256
+
257
+ For whole pages with metadata: `csid.pages("/users", {"limit": 100})` yields `Envelope`s, and `csid.get_raw(path, params)` returns one.
258
+
259
+ ### Errors
260
+
261
+ Every failure is a subclass of `CabalSpyIDError` carrying `.code`, `.status`, `.request_id`, `.docs` (a link to the error's documentation), and for validation errors `.parameter` and `.allowed`.
262
+
263
+ ```python
264
+ from cabalspy_id import NotFoundError, PlanUserLimitError, RateLimitError
265
+
266
+ try:
267
+ csid.profile(csid_id=claims["sub"])
268
+ except PlanUserLimitError:
269
+ show_upgrade_notice() # anon_ sub: no data until you upgrade
270
+ except NotFoundError:
271
+ pass # not one of your users
272
+ except RateLimitError as err:
273
+ print(err.retry_after)
274
+ ```
275
+
276
+ | Class | Status | Codes |
277
+ |---|---|---|
278
+ | `BadRequestError` | 400 | `missing_parameter`, `invalid_parameter`, `instance_required` |
279
+ | `AuthenticationError` | 401 | `missing_api_key` |
280
+ | `PermissionError` | 403 | `invalid_api_key`, `product_not_enabled`, `no_instance`, `scope_not_allowed` |
281
+ | `PlanUserLimitError` | 403 | `plan_user_limit` |
282
+ | `NotFoundError` | 404 | `profile_not_found`, `wallet_not_found`, `instance_not_found` |
283
+ | `RateLimitError` | 429 | `rate_limit_exceeded` |
284
+ | `ServerError` | 5xx | `internal_error`, `service_unavailable` |
285
+ | `APIConnectionError` | – | network failure, timeout |
286
+ | `OAuthError` | 4xx | OAuth errors from the login endpoints (`invalid_grant`, `invalid_client`, …) |
287
+ | `TokenVerificationError` | – | ID / access token failed verification |
288
+ | `WebhookVerificationError` | – | webhook signature or timestamp invalid |
289
+
290
+ `429`, `5xx` and network errors are retried with exponential backoff (default 2 retries); `Retry-After` wins. The rate limit of the last call is in `csid.last_rate_limit` (`X-RateLimit-Limit/Remaining/Reset`).
291
+
292
+ ## Token theses
293
+
294
+ A thesis is a short opinion on a token (up to 1000 characters), optionally tagged bull or bear. **Only holders post theses**: one of the author's verified wallets bought the token and still holds it. Every thesis shows the author's real position — what they bought when posting and their live PnL now (or "sold"); authors who keep their trading private show only that they hold. Anyone signed in can reply and react (`like` plus 🔥 🚀 💎 👀 😂 🤝 💀 🐂 🐻). The same theses appear in the widget, through the Data API and on every platform.
295
+
296
+ Writing needs the scope **`thesis`** in your login (`scope=["openid", "profile", "thesis"]`).
297
+
298
+ **Scope of reads.** Theses are shared across platforms. Every read (`list_theses`, `thesis_summary`, `latest_theses`, `ThesisClient`, the widget, the live sockets) takes `scope`: `global` (default, every platform) or `instance` (only theses written on your instance). In the Data API your key decides the instance; the public API and widget need your `client_id`.
299
+
300
+ ### Widget
301
+
302
+ ```html
303
+ <div data-csid-thesis data-chain="solana" data-mint="..." data-client-id="csid_..." data-theme="dark"></div>
304
+ <script src="https://id.cabalspy.xyz/widget.js" async></script>
305
+ <script>CSID.thesis.setAccessToken(accessToken) // optional: the signed-in person writes right away</script>
306
+ ```
307
+
308
+ ### Data API (server to server)
309
+
310
+ Reading works with any key, for every token and user. Writing happens **in the name of one of your users**: pass their `csid_id` (the `sub` of their ID token); they must have signed in to your app with the scope `thesis` and be within your plan's user limit.
311
+
312
+ ```python
313
+ theses = csid.list_theses(blockchain="solana", mint=mint, sort="new")
314
+ counts = csid.thesis_summary(blockchain="solana", mint=mint) # theses, bull, bear, authors, token
315
+
316
+ ok = csid.thesis_eligibility(blockchain="solana", mint=mint, csid_id=claims["sub"])
317
+ if ok["can_post"]:
318
+ t = csid.post_thesis(blockchain="solana", mint=mint, body="Accumulating here.", stance="bull", csid_id=claims["sub"])
319
+ ```
320
+
321
+ | Method | Endpoint |
322
+ |---|---|
323
+ | `list_theses(blockchain=, mint=, sort=, stance=, limit=, cursor=, csid_id=)` / `iter_theses()` | `GET /v1/theses` |
324
+ | `thesis_summary(blockchain=, mint=)` | `GET /v1/theses/summary` |
325
+ | `thesis(id, csid_id=)` | `GET /v1/theses/item` |
326
+ | `thesis_replies(id, limit=, cursor=)` / `iter_thesis_replies()` | `GET /v1/theses/replies` |
327
+ | `latest_theses(blockchain=, users="all" \| "instance", limit=, cursor=)` | `GET /v1/theses/latest` |
328
+ | `user_theses(csid_id= \| username=)` | `GET /v1/theses/user` |
329
+ | `thesis_eligibility(blockchain=, mint=, csid_id=)` | `GET /v1/theses/eligibility` |
330
+ | `post_thesis(blockchain=, mint=, body=, stance=, csid_id=)` | `POST /v1/theses` |
331
+ | `reply_to_thesis(thesis_id=, body=, csid_id=)` | `POST /v1/theses/replies` |
332
+ | `react_to_thesis(thesis_id=, emoji=, on=, csid_id=)` | `POST /v1/theses/reactions` |
333
+ | `report_thesis(thesis_id=, reason=, note=, csid_id=)` | `POST /v1/theses/report` |
334
+ | `delete_thesis(thesis_id=, csid_id=)` | `POST /v1/theses/delete` |
335
+
336
+ | `thesis_notifications(csid_id)` | `GET /v1/theses/notifications` |
337
+ | `appeal_thesis_decision(csid_id, notification_id, message)` | `POST /v1/theses/appeal` |
338
+
339
+ Every thesis carries `reacted_by` (the latest reactions with who reacted) and, for theses, `latest_replies` (a preview of the newest replies).
340
+
341
+ **Moderation.** Posts hidden after reports, pauses and moderator decisions reach the author as notifications (`id`, `kind`, `text`, `data`, `created_at`, `read`, `appealable`, `appeal`); `appealable` ones can be appealed within 30 days with 10-1000 characters. Through the Data API in your user's name: `thesis_notifications(csid_id)` / `appeal_thesis_decision(...)`; with the user's token: `ThesisClient.notifications()`, `mark_notifications_read()`, `appeal(id, message)`. `ThesisClient.me()` also returns `notices` (`unread`, `latest`, `url`) for a badge.
342
+
343
+ Thesis error codes: `not_a_holder`, `consent_required`, `scope_missing`, `plan_user_limit` (`PlanUserLimitError`), `posting_paused`, `reporting_paused`, `sandbox_user` (403, `PermissionError`); `invalid_body`, `duplicate` (400); `not_found` (404); `rate_limited` (429). Writes are never retried after a server or network error, so a post can't be duplicated; only the API's own rate limit is retried.
344
+
345
+ With `data-client-id` the widget loads the design saved in the dashboard; `data-theme`, `data-scope`, `data-sort`, `data-font`, `data-radius`, `data-bg`, `data-card`, `data-text`, `data-muted`, `data-border`, `data-accent`, `data-accent-text`, `data-bull`, `data-bear`, `data-show-header`, `data-show-sentiment`, `data-compact` and `data-height` override it.
346
+
347
+ ### Theses stream (every plan)
348
+
349
+ Live theses for the tokens you choose, e.g. to put markers on a chart. Up to 200 tokens per connection (or all), on every plan; subscriptions are sent again after every reconnect. Async, needs the `stream` extra.
350
+
351
+ ```python
352
+ async with AsyncCabalSpyID() as csid:
353
+ ts = csid.theses_stream(scope="global") # or "instance"
354
+
355
+ @ts.on("thesis")
356
+ def on_thesis(t):
357
+ add_marker(t["blockchain"], t["mint"], t)
358
+
359
+ await ts.connect() # returns on "ready"
360
+ await ts.subscribe([{"blockchain": "solana", "mint": MINT_A}, {"blockchain": "base", "mint": TOKEN_B}])
361
+ await ts.unsubscribe([{"blockchain": "base", "mint": TOKEN_B}])
362
+ # await ts.subscribe_all() / await ts.unsubscribe_all()
363
+ await ts.run_forever()
364
+
365
+ # or iterate {"event": ..., "data": ...}
366
+ async with csid.theses_stream() as ts:
367
+ await ts.subscribe_all()
368
+ async for ev in ts:
369
+ print(ev["event"], ev["data"])
370
+ ```
371
+
372
+ Events: `ready`, `subscribed`, `thesis`, `thesis.reply`, `thesis.reaction`, `thesis.removed`, `heartbeat`, `reconnecting`, `close`, `error`. A refused connection (`invalid_api_key`, …) is fatal; a bad frame (`too_many_tokens`, an invalid address) is reported as `error` and the connection stays.
373
+
374
+ Also over the trade stream (Pro plan): `csid.stream(events=["theses"], blockchain="solana", mint=mint)` emits `thesis`, `thesis.reply`, `thesis.reaction` and `thesis.removed` (or iterate `stream.theses()`). Your users' activity also arrives as webhooks: `thesis.created` (`event["data"]["thesis"]`), `thesis.reply` (`event["data"]["reply"]`) and `thesis.reaction` (`thesis_id`, `chain`, `mint`, `emoji`, `on`, `reactions`).
375
+
376
+ ### Public thesis API: `ThesisClient`
377
+
378
+ `https://id.cabalspy.xyz/thesis/v1` needs no API key: reading is open, writing uses the user's access token (scope `thesis`).
379
+
380
+ ```python
381
+ from cabalspy_id import ThesisClient
382
+
383
+ # client_id credits posts to your app; add scope="instance" to show only theses written on your platform
384
+ with ThesisClient(access_token=result.tokens["access_token"], client_id="csid_...") as theses:
385
+ page = theses.list("solana", mint, sort="top") # items, pagination, token, summary
386
+ if theses.me("solana", mint)["can_post"]:
387
+ theses.post("solana", mint, body="Strong community, still early.", stance="bull")
388
+ theses.react(page.items[0]["id"], "🚀")
389
+ ```
390
+
391
+ Methods: `list`, `iterate`, `summary`, `get`, `replies`, `by_user`, `me`, `post`, `reply`, `react`, `report`, `delete`, `notifications`, `mark_notifications_read`, `appeal`, `live`, `set_access_token`. `AsyncThesisClient` has the same methods as coroutines. Live updates for one token (async):
392
+
393
+ ```python
394
+ live = ThesisClient().live("solana", mint) # also takes scope=, client_id=
395
+ live.on("thesis", lambda t: print("new", t["body"])) # also: reply, reaction, removed
396
+ await live.connect()
397
+ await live.run_forever()
398
+ ```
399
+
400
+ ## Live stream
401
+
402
+ Every trade of your users the moment it is indexed, with the trader's profile (Pro plan and up). Async only, needs `pip install "cabalspy-id[stream]"`.
403
+
404
+ ```python
405
+ async with AsyncCabalSpyID() as csid:
406
+ stream = csid.stream(blockchain="solana") # omit blockchain for every chain
407
+ # events=["trades"] (default), ["theses"] or both; mint= theses of one token
408
+
409
+ @stream.on("trade")
410
+ def on_trade(t):
411
+ print(t["trader"]["username"], t["transaction_type"], t["token_symbol"], t["value_usd"])
412
+
413
+ await stream.connect() # returns once the server confirmed the subscription
414
+ await stream.run_forever() # until stream.close() or a fatal error
415
+
416
+ # or iterate
417
+ async with csid.stream() as stream:
418
+ async for trade in stream:
419
+ print(trade)
420
+ ```
421
+
422
+ The server sends a heartbeat every 30 seconds. The client reconnects with exponential backoff after a dropped connection or when heartbeats stop; configuration errors (`invalid_api_key`, `plan_required`, `scope_not_allowed`) are fatal and stop reconnecting. Events: `trade`, `thesis`, `thesis.reply`, `thesis.reaction`, `thesis.removed`, `subscribed`, `heartbeat`, `open`, `close`, `reconnecting`, `error`; handlers may be sync or async.
423
+
424
+ ## Webhooks
425
+
426
+ Deliveries follow the [Standard Webhooks](https://www.standardwebhooks.com) spec (`webhook-id`, `webhook-timestamp`, `webhook-signature`, secret `whsec_...`) and are retried for about 2 days. Events: `wallet.verified`, `wallet.removed`, `profile.updated`, `kol.granted`, `consent.revoked`, `thesis.created`, `thesis.reply`, `thesis.reaction` — only for users within the plan's limit.
427
+
428
+ ```python
429
+ from cabalspy_id import verify_webhook
430
+
431
+ # pass the RAW body, exactly as received
432
+ event = verify_webhook(request.body, request.headers, os.environ["CSID_WEBHOOK_SECRET"])
433
+
434
+ if event["type"] == "consent.revoked":
435
+ delete_user_data(event["data"]["sub"])
436
+ ```
437
+
438
+ Timestamps older than 5 minutes are rejected (`tolerance=`). During a secret roll several signatures are sent; any match is accepted. Dedupe on `event["id"]`, it is the same on every retry. `sign_webhook()` builds test requests for your receiver.
439
+
440
+ ## Configuration
441
+
442
+ ```python
443
+ CabalSpyID(
444
+ api_key=None, # env fallback: CABALSPY_ID_API_KEY, then CABALSPY_API_KEY
445
+ instance=None, # client_id, when the account has several instances
446
+ base_url="https://api-id.cabalspy.xyz/v1",
447
+ stream_url="wss://api-id.cabalspy.xyz/v1/stream",
448
+ timeout=30.0,
449
+ max_retries=2,
450
+ headers={},
451
+ http_client=None, # bring your own httpx.Client / httpx.AsyncClient
452
+ )
453
+
454
+ CabalSpyIDAuth(
455
+ "csid_...",
456
+ client_secret=None, # csid_sk_..., server apps only
457
+ redirect_uri=None,
458
+ issuer="https://id.cabalspy.xyz",
459
+ token_endpoint_auth_method=None, # client_secret_basic | client_secret_post | none
460
+ clock_tolerance=60,
461
+ timeout=15.0,
462
+ http_client=None,
463
+ )
464
+ ```
465
+
466
+ Keep the API key and the client secret on your server; read them from the environment or a secret manager.
467
+
468
+ ## Pricing
469
+
470
+ Per month and registrable domain. Everyone can always sign in; above the limit the platform receives no data for the extra users until it upgrades.
471
+
472
+ | Plan | Price / month | Users with data | API rate | |
473
+ |---|---|---|---|---|
474
+ | Free | $0 | 50 | 1 req/s | |
475
+ | Starter | $49 | 200 | 5 req/s | |
476
+ | Pro | $149 | 500 | 10 req/s | + live stream |
477
+ | Business | $499 | 1,000 | 25 req/s | |
478
+ | Enterprise | custom | unlimited | custom + SLA | `scope=global` |
479
+
480
+ Yearly billing is 20% off. Card or crypto at [apidashboard.cabalspy.xyz/billing/csid](https://apidashboard.cabalspy.xyz/billing/csid).
481
+
482
+ ## Docs
483
+
484
+ [docs.cabalspy.xyz/cabalspy-id](https://docs.cabalspy.xyz/cabalspy-id) · Discovery: [id.cabalspy.xyz/.well-known/openid-configuration](https://id.cabalspy.xyz/.well-known/openid-configuration)
485
+
486
+ ## Related
487
+
488
+ - [CabalSpy ID](https://id.cabalspy.xyz) · [leaderboard](https://id.cabalspy.xyz/leaderboard) · [API dashboard](https://apidashboard.cabalspy.xyz)
489
+ - CabalSpy ID SDKs: [TypeScript](https://www.npmjs.com/package/cabalspy-id) · [Python](https://pypi.org/project/cabalspy-id/)
490
+ - CabalSpy API SDKs (KOL and smart money wallet tracking): [TypeScript](https://www.npmjs.com/package/cabalspy) · [Python](https://pypi.org/project/cabalspy/) · [Rust](https://crates.io/crates/cabalspy)
491
+ - [SDK overview](https://www.cabalspy.xyz/sdks/) · [MCP server](https://www.cabalspy.xyz/mcp/) · [CabalSpy Terminal](https://app.cabalspy.xyz/)
492
+
493
+ ## License
494
+
495
+ MIT