cabalspy-id 0.5.1 → 0.5.2
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.
- package/CHANGELOG.md +146 -112
- package/LICENSE +21 -21
- package/README.md +505 -475
- package/dist/api.cjs +5 -3
- package/dist/api.cjs.map +1 -1
- package/dist/api.d.cts +1 -1
- package/dist/api.d.ts +1 -1
- package/dist/api.js +16 -14
- package/dist/chunk-A2ZAFAWC.cjs.map +1 -1
- package/dist/chunk-ABFIPCBY.cjs +109 -0
- package/dist/chunk-ABFIPCBY.cjs.map +1 -0
- package/dist/{chunk-Z3UPDOBY.js → chunk-DBYCRBVS.js} +9 -7
- package/dist/chunk-DBYCRBVS.js.map +1 -0
- package/dist/{chunk-P3MDLWLN.cjs → chunk-PKFIMBPW.cjs} +113 -54
- package/dist/chunk-PKFIMBPW.cjs.map +1 -0
- package/dist/{chunk-L4RHSWOA.cjs → chunk-ULEBNY2A.cjs} +121 -212
- package/dist/chunk-ULEBNY2A.cjs.map +1 -0
- package/dist/{chunk-X3OFGGSE.cjs → chunk-VDFCMYLR.cjs} +50 -48
- package/dist/chunk-VDFCMYLR.cjs.map +1 -0
- package/dist/chunk-VOTYIQAY.js +109 -0
- package/dist/chunk-VOTYIQAY.js.map +1 -0
- package/dist/{chunk-3LKLY5WQ.js → chunk-XLBO2RSL.js} +15 -106
- package/dist/chunk-XLBO2RSL.js.map +1 -0
- package/dist/{chunk-3P7QK2PG.js → chunk-Z23B5TY6.js} +89 -30
- package/dist/chunk-Z23B5TY6.js.map +1 -0
- package/dist/index.cjs +13 -5
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +3 -3
- package/dist/index.d.ts +3 -3
- package/dist/index.js +25 -17
- package/dist/index.js.map +1 -1
- package/dist/react.cjs +55 -2
- package/dist/react.cjs.map +1 -1
- package/dist/react.d.cts +28 -5
- package/dist/react.d.ts +28 -5
- package/dist/react.js +55 -2
- package/dist/react.js.map +1 -1
- package/dist/thesis.cjs +11 -3
- package/dist/thesis.cjs.map +1 -1
- package/dist/thesis.d.cts +42 -11
- package/dist/thesis.d.ts +42 -11
- package/dist/thesis.js +24 -16
- package/dist/{users-BDLkwW1p.d.cts → users-3DAw88_U.d.cts} +4 -4
- package/dist/{users-CDlDRuyv.d.ts → users-B4r33i4O.d.ts} +4 -4
- package/dist/widget.cjs +28 -12
- package/dist/widget.cjs.map +1 -1
- package/dist/widget.d.cts +37 -6
- package/dist/widget.d.ts +37 -6
- package/dist/widget.js +17 -1
- package/dist/widget.js.map +1 -1
- package/package.json +147 -147
- package/dist/chunk-3LKLY5WQ.js.map +0 -1
- package/dist/chunk-3P7QK2PG.js.map +0 -1
- package/dist/chunk-L4RHSWOA.cjs.map +0 -1
- package/dist/chunk-P3MDLWLN.cjs.map +0 -1
- package/dist/chunk-X3OFGGSE.cjs.map +0 -1
- package/dist/chunk-Z3UPDOBY.js.map +0 -1
package/README.md
CHANGED
|
@@ -1,475 +1,505 @@
|
|
|
1
|
-
# CabalSpy ID — SocialFi API & SDK
|
|
2
|
-
|
|
3
|
-
[](https://www.npmjs.com/package/cabalspy-id) [](https://github.com/CabalSpy/cabalspy-id-js/actions/workflows/ci.yml) [](https://www.npmjs.com/package/cabalspy-id) [](LICENSE)
|
|
4
|
-
|
|
5
|
-
Official TypeScript SDK and React SocialFi widget 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**.
|
|
6
|
-
|
|
7
|
-
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.
|
|
8
|
-
|
|
9
|
-
[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)
|
|
10
|
-
|
|
11
|
-
```bash
|
|
12
|
-
npm install cabalspy-id
|
|
13
|
-
```
|
|
14
|
-
|
|
15
|
-
One package, everything you need:
|
|
16
|
-
|
|
17
|
-
| | |
|
|
18
|
-
|---|---|
|
|
19
|
-
| `CabalSpyIDAuth` | OpenID Connect for your server: PKCE, code exchange, refresh, revoke, userinfo, ID token verification (ES256 / JWKS) |
|
|
20
|
-
| `cabalspy-id/react` | `<CabalSpyLogin />` button, `<CabalSpyTheses />` widget and `useCabalSpyID()` hook |
|
|
21
|
-
| `ThesisClient` | public token thesis API for your own UI in the browser (user access token) with `live()` updates |
|
|
22
|
-
| `CabalSpyID` | Data API client for `https://api-id.cabalspy.xyz/v1`, typed responses, pagination, live trade stream |
|
|
23
|
-
| `verifyWebhook` | Standard Webhooks signature verification |
|
|
24
|
-
|
|
25
|
-
Zero runtime dependencies: crypto runs on WebCrypto. Works in Node 18+, browsers, Deno, Bun and edge runtimes. ESM and CommonJS.
|
|
26
|
-
|
|
27
|
-
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**.
|
|
28
|
-
|
|
29
|
-
## What can you build
|
|
30
|
-
|
|
31
|
-
- **Social trading feed** — every trade of your users live, with their X profile and PnL
|
|
32
|
-
- **Leaderboards** — rank your users by PnL, volume, win rate or trades
|
|
33
|
-
- **Copy trading** — follow verified wallets of real, identified traders
|
|
34
|
-
- **Token theses** — holders-only opinions on a token with the author's real position, as a SocialFi widget or your own UI
|
|
35
|
-
- **Trader profiles** — X profile, verified wallets, holdings, PnL calendar
|
|
36
|
-
- **KOL badges** — reviewed badges for influential traders, shown wherever they sign in
|
|
37
|
-
|
|
38
|
-
## Packages
|
|
39
|
-
|
|
40
|
-
One codebase, one version line. The focused packages re-export parts of `cabalspy-id`, so they always behave exactly the same.
|
|
41
|
-
|
|
42
|
-
| Package | npm | PyPI | What it contains |
|
|
43
|
-
|---|---|---|---|
|
|
44
|
-
| **cabalspy-id** | [`cabalspy-id`](https://www.npmjs.com/package/cabalspy-id) | [`cabalspy-id`](https://pypi.org/project/cabalspy-id/) | everything below |
|
|
45
|
-
| **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`) |
|
|
46
|
-
| **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`) |
|
|
47
|
-
| **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`) |
|
|
48
|
-
|
|
49
|
-
### Entry points
|
|
50
|
-
|
|
51
|
-
| Import | Contents |
|
|
52
|
-
|---|---|
|
|
53
|
-
| `cabalspy-id` | everything |
|
|
54
|
-
| `cabalspy-id/api` | `CabalSpyID`, `CabalSpyIDStream`, `CabalSpyIDAuth`, `verifyWebhook`, errors and Data API types |
|
|
55
|
-
| `cabalspy-id/thesis` | `CabalSpyID` (thesis methods), `ThesisClient`, `ThesisLive`, `ThesesStream`, thesis types |
|
|
56
|
-
| `cabalspy-id/widget` | browser only: `mountLoginButton`, `mountTheses`, `setThesisAccessToken`, `startLogin`, `onWidgetEvents`, `loadCabalSpyIDWidget` |
|
|
57
|
-
| `cabalspy-id/react` (= `cabalspy-id/widget/react`) | `<CabalSpyLogin />`, `<CabalSpyTheses />`, `useCabalSpyID()` |
|
|
58
|
-
|
|
59
|
-
Classes are shared between entry points (`instanceof` works across them, ESM and CommonJS).
|
|
60
|
-
|
|
61
|
-
```ts
|
|
62
|
-
import { mountLoginButton, mountTheses } from 'cabalspy-id/widget';
|
|
63
|
-
|
|
64
|
-
mountLoginButton('#login', { clientId: 'csid_...', shape: 'pill', onLogin: ({ idToken }) => sendToBackend(idToken) });
|
|
65
|
-
const theses = mountTheses('#theses', { chain: 'solana', mint, clientId: 'csid_...', theme: 'dark', compact: true });
|
|
66
|
-
theses.update({ theme: 'light' });
|
|
67
|
-
```
|
|
68
|
-
|
|
69
|
-
## Login button
|
|
70
|
-
|
|
71
|
-
Plain HTML, no build step:
|
|
72
|
-
|
|
73
|
-
```html
|
|
74
|
-
<div data-csid-login data-client-id="csid_..."></div>
|
|
75
|
-
<script src="https://id.cabalspy.xyz/widget.js" async></script>
|
|
76
|
-
```
|
|
77
|
-
|
|
78
|
-
React:
|
|
79
|
-
|
|
80
|
-
```tsx
|
|
81
|
-
import { CabalSpyLogin, useCabalSpyID } from 'cabalspy-id/react';
|
|
82
|
-
|
|
83
|
-
export function Header() {
|
|
84
|
-
const { user, limited } = useCabalSpyID({ clientId: 'csid_...' });
|
|
85
|
-
return (
|
|
86
|
-
<CabalSpyLogin
|
|
87
|
-
clientId="csid_..."
|
|
88
|
-
scope={['openid', 'profile', 'wallets']}
|
|
89
|
-
theme="dark"
|
|
90
|
-
shape="pill"
|
|
91
|
-
onLogin={({ idToken }) => fetch('/api/session', { method: 'POST', body: JSON.stringify({ idToken }) })}
|
|
92
|
-
onLogout={() => fetch('/api/session', { method: 'DELETE' })}
|
|
93
|
-
/>
|
|
94
|
-
);
|
|
95
|
-
}
|
|
96
|
-
```
|
|
97
|
-
|
|
98
|
-
`<CabalSpyLogin>` loads `widget.js` once and maps its props to the widget's `data-*` attributes:
|
|
99
|
-
|
|
100
|
-
| Prop | Attribute | |
|
|
101
|
-
|---|---|---|
|
|
102
|
-
| `clientId` | `data-client-id` | your instance's client_id (required) |
|
|
103
|
-
| `theme`, `color`, `textColor`, `shape`, `size`, `label`, `fullWidth`, `showLogo` | `data-theme` … `data-show-logo` | override the design from the dashboard's button builder |
|
|
104
|
-
| `href` | `data-href` | **server apps:** your login route (you run the OIDC flow with your secret) |
|
|
105
|
-
| `redirectUri`, `scope` | `data-redirect-uri`, `data-scope` | **browser apps:** the widget runs PKCE itself |
|
|
106
|
-
| `user`, `logoutHref` | `data-user`, `data-logout-href` | **server apps:** show the signed-in person's avatar menu |
|
|
107
|
-
| `onLogin`, `onLogout`, `onError` | events `csid:login`, `csid:logout`, `csid:error` | |
|
|
108
|
-
|
|
109
|
-
Browser apps (public clients) get `{ user, idToken, accessToken, expiresIn }` in `onLogin`. **Always send `idToken` to your backend and verify it there** before you trust it:
|
|
110
|
-
|
|
111
|
-
```ts
|
|
112
|
-
import { CabalSpyIDAuth } from 'cabalspy-id';
|
|
113
|
-
|
|
114
|
-
const auth = new CabalSpyIDAuth({ clientId: 'csid_...' });
|
|
115
|
-
const claims = await auth.verifyIdToken(idToken); // ES256 via JWKS: iss, aud, azp, exp
|
|
116
|
-
```
|
|
117
|
-
|
|
118
|
-
`useCabalSpyID({ clientId })` returns `{ user, idToken, accessToken, limited, ready, login(), signOut() }`.
|
|
119
|
-
|
|
120
|
-
## Server-side login (OpenID Connect)
|
|
121
|
-
|
|
122
|
-
Authorization code flow with PKCE S256 (required for every client), state and nonce.
|
|
123
|
-
|
|
124
|
-
```ts
|
|
125
|
-
import { CabalSpyIDAuth, isLimited } from 'cabalspy-id';
|
|
126
|
-
|
|
127
|
-
const auth = new CabalSpyIDAuth({
|
|
128
|
-
clientId: process.env.CSID_CLIENT_ID!, // csid_...
|
|
129
|
-
clientSecret: process.env.CSID_CLIENT_SECRET, // csid_sk_... (omit for public clients)
|
|
130
|
-
redirectUri: 'https://your.app/callback', // registered in the dashboard
|
|
131
|
-
});
|
|
132
|
-
|
|
133
|
-
// GET /login
|
|
134
|
-
const login = await auth.createAuthorizationUrl({ scope: ['openid', 'profile', 'wallets', 'offline_access'] });
|
|
135
|
-
// keep login.state, login.nonce and login.codeVerifier (session or signed http-only cookie)
|
|
136
|
-
redirect(login.url);
|
|
137
|
-
|
|
138
|
-
// GET /callback
|
|
139
|
-
const { tokens, claims } = await auth.handleCallback(request.url, stored);
|
|
140
|
-
// checks error, state and iss, exchanges the code, verifies the ID token and its nonce
|
|
141
|
-
|
|
142
|
-
if (isLimited(claims)) {
|
|
143
|
-
// over your plan's user limit: anonymous sub "anon_...", no profile or wallet data
|
|
144
|
-
} else {
|
|
145
|
-
const user = await auth.userinfo(tokens.access_token); // x profile, wallets, onchain
|
|
146
|
-
}
|
|
147
|
-
```
|
|
148
|
-
|
|
149
|
-
| Method | |
|
|
150
|
-
|---|---|
|
|
151
|
-
| `discover()` | the discovery document (cached) |
|
|
152
|
-
| `createAuthorizationUrl({ scope?, prompt?, redirectUri?, state?, nonce? })` | returns `{ url, state, nonce, codeVerifier, redirectUri }` |
|
|
153
|
-
| `handleCallback(url, { state, nonce, codeVerifier })` | full callback handling, returns `{ tokens, claims }` |
|
|
154
|
-
| `exchangeCode({ code, codeVerifier, redirectUri? })` | `POST /oauth/token`, authorization_code |
|
|
155
|
-
| `refresh(refreshToken)` | `POST /oauth/token`, refresh_token. Tokens rotate: store the new one; reusing an old one revokes the chain |
|
|
156
|
-
| `revoke(token)` | `POST /oauth/revoke` |
|
|
157
|
-
| `userinfo(accessToken)` | `GET /oauth/userinfo` |
|
|
158
|
-
| `verifyIdToken(idToken, { nonce?, maxAge? })` | ES256 signature via JWKS, iss, aud, azp, exp, nonce |
|
|
159
|
-
| `verifyAccessToken(accessToken)` | for access tokens your frontend sends to your API (`typ: at+jwt`) |
|
|
160
|
-
|
|
161
|
-
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` (public clients). Access and ID tokens live 15 minutes; refresh tokens 30 days of inactivity, 90 days absolute.
|
|
162
|
-
|
|
163
|
-
**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. `isLimited(claims)` checks both.
|
|
164
|
-
|
|
165
|
-
A runnable server is in [examples/login-server.mjs](examples/login-server.mjs).
|
|
166
|
-
|
|
167
|
-
## Users without X
|
|
168
|
-
|
|
169
|
-
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:
|
|
170
|
-
|
|
171
|
-
| Field | With X | Without X |
|
|
172
|
-
|---|---|---|
|
|
173
|
-
| `preferred_username` (claims) / `username` | X handle, e.g. `alice` | `tg.<telegram name>` (Telegram) or `id.<hex>` (Google) — always contains a dot |
|
|
174
|
-
| `x` (userinfo, `/v1/profile`) | X profile / stats | `null` |
|
|
175
|
-
| `twitter` (profiles, trading profile blocks, leaderboards) | `https://x.com/<handle>` | `null` |
|
|
176
|
-
| `telegram` (profiles, trading profile blocks, leaderboards) | `https://t.me/<name>` or `""` | `https://t.me/<name>` or `""` |
|
|
177
|
-
| `name`, `picture` / `image_url` | from X | from Telegram or Google |
|
|
178
|
-
|
|
179
|
-
`profile_url` (`https://id.cabalspy.xyz/u/<handle>`) works for everyone. Thesis authors use the same handles.
|
|
180
|
-
|
|
181
|
-
```ts
|
|
182
|
-
import { hasX, handleKind } from 'cabalspy-id';
|
|
183
|
-
|
|
184
|
-
hasX(claims); // false for Telegram/Google sign-ins
|
|
185
|
-
handleKind('tg.alice'); // 'telegram'
|
|
186
|
-
const xLink = profile.twitter ?? null; // never build x.com links from tg./id. handles
|
|
187
|
-
```
|
|
188
|
-
|
|
189
|
-
## Data API
|
|
190
|
-
|
|
191
|
-
```ts
|
|
192
|
-
import { CabalSpyID } from 'cabalspy-id';
|
|
193
|
-
|
|
194
|
-
const csid = new CabalSpyID({ apiKey: process.env.CABALSPY_API_KEY });
|
|
195
|
-
|
|
196
|
-
const me = await csid.me(); // instance, plan, rate limit, users with data
|
|
197
|
-
|
|
198
|
-
// The person who just signed in: look them up by the `sub` of their ID token
|
|
199
|
-
const profile = await csid.profile({ csid_id: claims.sub });
|
|
200
|
-
const wallet = profile.wallets.find((w) => w.chains.includes('solana'))!;
|
|
201
|
-
|
|
202
|
-
// Full trading profile for a period, same numbers as the CabalSpy API
|
|
203
|
-
const t = await csid.tracker({ blockchain: 'solana', address: wallet.address, period: '7d' });
|
|
204
|
-
console.log(t.period_stats.realized_pnl_usd, t.period_win_rate_distribution.win_rate_percentage);
|
|
205
|
-
|
|
206
|
-
// Your users ranked by PnL
|
|
207
|
-
const board = await csid.leaderboard({ blockchain: 'solana', period: '7d', sort: 'pnl_usd', limit: 25 });
|
|
208
|
-
```
|
|
209
|
-
|
|
210
|
-
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.
|
|
211
|
-
|
|
212
|
-
| Method | Endpoint |
|
|
213
|
-
|---|---|
|
|
214
|
-
| `health()` | `GET /v1/health` (no key needed) |
|
|
215
|
-
| `me()` | `GET /v1/me` |
|
|
216
|
-
| `users({ limit?, kol?, cursor?, scope? })` | `GET /v1/users` |
|
|
217
|
-
| `profile({ username } \| { csid_id } \| { address })` | `GET /v1/profile` |
|
|
218
|
-
| `walletLookup(address)` | `GET /v1/wallets/lookup` |
|
|
219
|
-
| `tracker({ blockchain, address, period? })` | `GET /v1/wallets/tracker` |
|
|
220
|
-
| `history({ blockchain, address, limit?, cursor? })` | `GET /v1/wallets/history` |
|
|
221
|
-
| `holdings({ blockchain, address })` | `GET /v1/wallets/holdings` |
|
|
222
|
-
| `pnlCalendar({ blockchain, address })` | `GET /v1/wallets/pnl_calendar` |
|
|
223
|
-
| `leaderboard({ blockchain, period?, sort?, limit?, cursor?, scope? })` | `GET /v1/wallets/leaderboard` |
|
|
224
|
-
| `latestTransactions({ blockchain?, limit? })` | `GET /v1/transactions/latest` |
|
|
225
|
-
|
|
226
|
-
Chains: `solana`, `bnb`, `base`, `eth`, `rh`. Periods: `6h`, `1d`, `7d`, `30d`. Leaderboard sorts: `volume`, `pnl`, `pnl_usd`, `win_rate`, `trades`. Every response is fully typed.
|
|
227
|
-
|
|
228
|
-
If your account has several CabalSpy ID instances, pass `instance: 'csid_...'` (the client_id) to the constructor.
|
|
229
|
-
|
|
230
|
-
### Pagination
|
|
231
|
-
|
|
232
|
-
Async iterators follow `next_cursor` for you:
|
|
233
|
-
|
|
234
|
-
```ts
|
|
235
|
-
for await (const user of csid.iterUsers({ kol: true })) console.log(user.username);
|
|
236
|
-
for await (const trade of csid.iterHistory({ blockchain: 'solana', address })) console.log(trade.tx_signature);
|
|
237
|
-
for await (const entry of csid.iterLeaderboard({ blockchain: 'base', period: '30d' })) console.log(entry.rank);
|
|
238
|
-
```
|
|
239
|
-
|
|
240
|
-
For whole pages with metadata, use the `*Raw` methods (`usersRaw`, `historyRaw`, `leaderboardRaw`) with `csid.pages()`.
|
|
241
|
-
|
|
242
|
-
### Errors
|
|
243
|
-
|
|
244
|
-
Every failure is a typed subclass of `CabalSpyIDError` carrying `.code`, `.status`, `.requestId`, `.docs` (a link to the error's documentation), and for validation errors `.parameter` and `.allowed`.
|
|
245
|
-
|
|
246
|
-
```ts
|
|
247
|
-
import { NotFoundError, PlanUserLimitError, RateLimitError } from 'cabalspy-id';
|
|
248
|
-
|
|
249
|
-
try {
|
|
250
|
-
await csid.profile({ csid_id: claims.sub });
|
|
251
|
-
} catch (err) {
|
|
252
|
-
if (err instanceof PlanUserLimitError) return showUpgradeNotice(); // anon_ sub: no data until you upgrade
|
|
253
|
-
if (err instanceof NotFoundError) return null; // not one of your users
|
|
254
|
-
if (err instanceof RateLimitError) console.log(err.retryAfter);
|
|
255
|
-
throw err;
|
|
256
|
-
}
|
|
257
|
-
```
|
|
258
|
-
|
|
259
|
-
| Class | Status | Codes |
|
|
260
|
-
|---|---|---|
|
|
261
|
-
| `BadRequestError` | 400 | `missing_parameter`, `invalid_parameter`, `instance_required` |
|
|
262
|
-
| `AuthenticationError` | 401 | `missing_api_key` |
|
|
263
|
-
| `PermissionError` | 403 | `invalid_api_key`, `product_not_enabled`, `no_instance`, `scope_not_allowed` |
|
|
264
|
-
| `PlanUserLimitError` | 403 | `plan_user_limit` |
|
|
265
|
-
| `NotFoundError` | 404 | `profile_not_found`, `wallet_not_found`, `instance_not_found` |
|
|
266
|
-
| `RateLimitError` | 429 | `rate_limit_exceeded` |
|
|
267
|
-
| `ServerError` | 5xx | `internal_error`, `service_unavailable` |
|
|
268
|
-
| `ConnectionError` | – | network failure, timeout |
|
|
269
|
-
| `OAuthError` | 4xx | OAuth errors from the login endpoints (`invalid_grant`, `invalid_client`, …) |
|
|
270
|
-
| `TokenVerificationError` | – | ID / access token failed verification |
|
|
271
|
-
| `WebhookVerificationError` | – | webhook signature or timestamp invalid |
|
|
272
|
-
|
|
273
|
-
`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.lastRateLimit` (`X-RateLimit-Limit/Remaining/Reset`).
|
|
274
|
-
|
|
275
|
-
## Token theses
|
|
276
|
-
|
|
277
|
-
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.
|
|
278
|
-
|
|
279
|
-
Writing needs the scope **`thesis`** in your login (`scope: ['openid', 'profile', 'thesis']`).
|
|
280
|
-
|
|
281
|
-
**Scope of reads.** Theses are shared across platforms. Every read (Data API `listTheses`, `thesisSummary`, `latestTheses`; `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
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
```
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
const
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
const
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
|
341
|
-
|
|
342
|
-
| `
|
|
343
|
-
| `
|
|
344
|
-
| `
|
|
345
|
-
| `
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
```
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
```
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
1
|
+
# CabalSpy ID — SocialFi API & SDK
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/cabalspy-id) [](https://github.com/CabalSpy/cabalspy-id-js/actions/workflows/ci.yml) [](https://www.npmjs.com/package/cabalspy-id) [](LICENSE)
|
|
4
|
+
|
|
5
|
+
Official TypeScript SDK and React SocialFi widget 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**.
|
|
6
|
+
|
|
7
|
+
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.
|
|
8
|
+
|
|
9
|
+
[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)
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npm install cabalspy-id
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
One package, everything you need:
|
|
16
|
+
|
|
17
|
+
| | |
|
|
18
|
+
|---|---|
|
|
19
|
+
| `CabalSpyIDAuth` | OpenID Connect for your server: PKCE, code exchange, refresh, revoke, userinfo, ID token verification (ES256 / JWKS) |
|
|
20
|
+
| `cabalspy-id/react` | `<CabalSpyLogin />` button, `<CabalSpyTheses />` widget and `useCabalSpyID()` hook |
|
|
21
|
+
| `ThesisClient` | public token thesis API for your own UI in the browser (your public `clientId` + user access token) with `live()` updates |
|
|
22
|
+
| `CabalSpyID` | Data API client for `https://api-id.cabalspy.xyz/v1`, typed responses, pagination, live trade stream |
|
|
23
|
+
| `verifyWebhook` | Standard Webhooks signature verification |
|
|
24
|
+
|
|
25
|
+
Zero runtime dependencies: crypto runs on WebCrypto. Works in Node 18+, browsers, Deno, Bun and edge runtimes. ESM and CommonJS.
|
|
26
|
+
|
|
27
|
+
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**.
|
|
28
|
+
|
|
29
|
+
## What can you build
|
|
30
|
+
|
|
31
|
+
- **Social trading feed** — every trade of your users live, with their X profile and PnL
|
|
32
|
+
- **Leaderboards** — rank your users by PnL, volume, win rate or trades
|
|
33
|
+
- **Copy trading** — follow verified wallets of real, identified traders
|
|
34
|
+
- **Token theses** — holders-only opinions on a token with the author's real position, as a SocialFi widget or your own UI
|
|
35
|
+
- **Trader profiles** — X profile, verified wallets, holdings, PnL calendar
|
|
36
|
+
- **KOL badges** — reviewed badges for influential traders, shown wherever they sign in
|
|
37
|
+
|
|
38
|
+
## Packages
|
|
39
|
+
|
|
40
|
+
One codebase, one version line. The focused packages re-export parts of `cabalspy-id`, so they always behave exactly the same.
|
|
41
|
+
|
|
42
|
+
| Package | npm | PyPI | What it contains |
|
|
43
|
+
|---|---|---|---|
|
|
44
|
+
| **cabalspy-id** | [`cabalspy-id`](https://www.npmjs.com/package/cabalspy-id) | [`cabalspy-id`](https://pypi.org/project/cabalspy-id/) | everything below |
|
|
45
|
+
| **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`) |
|
|
46
|
+
| **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`) |
|
|
47
|
+
| **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`) |
|
|
48
|
+
|
|
49
|
+
### Entry points
|
|
50
|
+
|
|
51
|
+
| Import | Contents |
|
|
52
|
+
|---|---|
|
|
53
|
+
| `cabalspy-id` | everything |
|
|
54
|
+
| `cabalspy-id/api` | `CabalSpyID`, `CabalSpyIDStream`, `CabalSpyIDAuth`, `verifyWebhook`, errors and Data API types |
|
|
55
|
+
| `cabalspy-id/thesis` | `CabalSpyID` (thesis methods), `ThesisClient`, `ThesisLive`, `ThesesStream`, thesis types |
|
|
56
|
+
| `cabalspy-id/widget` | browser only: `mountLoginButton`, `mountTheses`, `setThesisAccessToken`, `setThesisSignInHandler`, `startLogin`, `onWidgetEvents`, `loadCabalSpyIDWidget` |
|
|
57
|
+
| `cabalspy-id/react` (= `cabalspy-id/widget/react`) | `<CabalSpyLogin />`, `<CabalSpyTheses />`, `useCabalSpyID()` |
|
|
58
|
+
|
|
59
|
+
Classes are shared between entry points (`instanceof` works across them, ESM and CommonJS).
|
|
60
|
+
|
|
61
|
+
```ts
|
|
62
|
+
import { mountLoginButton, mountTheses } from 'cabalspy-id/widget';
|
|
63
|
+
|
|
64
|
+
mountLoginButton('#login', { clientId: 'csid_...', shape: 'pill', onLogin: ({ idToken }) => sendToBackend(idToken) });
|
|
65
|
+
const theses = mountTheses('#theses', { chain: 'solana', mint, clientId: 'csid_...', theme: 'dark', compact: true });
|
|
66
|
+
theses.update({ theme: 'light' });
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## Login button
|
|
70
|
+
|
|
71
|
+
Plain HTML, no build step:
|
|
72
|
+
|
|
73
|
+
```html
|
|
74
|
+
<div data-csid-login data-client-id="csid_..."></div>
|
|
75
|
+
<script src="https://id.cabalspy.xyz/widget.js" async></script>
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
React:
|
|
79
|
+
|
|
80
|
+
```tsx
|
|
81
|
+
import { CabalSpyLogin, useCabalSpyID } from 'cabalspy-id/react';
|
|
82
|
+
|
|
83
|
+
export function Header() {
|
|
84
|
+
const { user, limited } = useCabalSpyID({ clientId: 'csid_...' });
|
|
85
|
+
return (
|
|
86
|
+
<CabalSpyLogin
|
|
87
|
+
clientId="csid_..."
|
|
88
|
+
scope={['openid', 'profile', 'wallets']}
|
|
89
|
+
theme="dark"
|
|
90
|
+
shape="pill"
|
|
91
|
+
onLogin={({ idToken }) => fetch('/api/session', { method: 'POST', body: JSON.stringify({ idToken }) })}
|
|
92
|
+
onLogout={() => fetch('/api/session', { method: 'DELETE' })}
|
|
93
|
+
/>
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
`<CabalSpyLogin>` loads `widget.js` once and maps its props to the widget's `data-*` attributes:
|
|
99
|
+
|
|
100
|
+
| Prop | Attribute | |
|
|
101
|
+
|---|---|---|
|
|
102
|
+
| `clientId` | `data-client-id` | your instance's client_id (required) |
|
|
103
|
+
| `theme`, `color`, `textColor`, `shape`, `size`, `label`, `fullWidth`, `showLogo` | `data-theme` … `data-show-logo` | override the design from the dashboard's button builder |
|
|
104
|
+
| `href` | `data-href` | **server apps:** your login route (you run the OIDC flow with your secret) |
|
|
105
|
+
| `redirectUri`, `scope` | `data-redirect-uri`, `data-scope` | **browser apps:** the widget runs PKCE itself |
|
|
106
|
+
| `user`, `logoutHref` | `data-user`, `data-logout-href` | **server apps:** show the signed-in person's avatar menu |
|
|
107
|
+
| `onLogin`, `onLogout`, `onError` | events `csid:login`, `csid:logout`, `csid:error` | |
|
|
108
|
+
|
|
109
|
+
Browser apps (public clients) get `{ user, idToken, accessToken, expiresIn }` in `onLogin`. **Always send `idToken` to your backend and verify it there** before you trust it:
|
|
110
|
+
|
|
111
|
+
```ts
|
|
112
|
+
import { CabalSpyIDAuth } from 'cabalspy-id';
|
|
113
|
+
|
|
114
|
+
const auth = new CabalSpyIDAuth({ clientId: 'csid_...' });
|
|
115
|
+
const claims = await auth.verifyIdToken(idToken); // ES256 via JWKS: iss, aud, azp, exp
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
`useCabalSpyID({ clientId })` returns `{ user, idToken, accessToken, limited, ready, login(), signOut() }`.
|
|
119
|
+
|
|
120
|
+
## Server-side login (OpenID Connect)
|
|
121
|
+
|
|
122
|
+
Authorization code flow with PKCE S256 (required for every client), state and nonce.
|
|
123
|
+
|
|
124
|
+
```ts
|
|
125
|
+
import { CabalSpyIDAuth, isLimited } from 'cabalspy-id';
|
|
126
|
+
|
|
127
|
+
const auth = new CabalSpyIDAuth({
|
|
128
|
+
clientId: process.env.CSID_CLIENT_ID!, // csid_...
|
|
129
|
+
clientSecret: process.env.CSID_CLIENT_SECRET, // csid_sk_... (omit for public clients)
|
|
130
|
+
redirectUri: 'https://your.app/callback', // registered in the dashboard
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
// GET /login
|
|
134
|
+
const login = await auth.createAuthorizationUrl({ scope: ['openid', 'profile', 'wallets', 'offline_access'] });
|
|
135
|
+
// keep login.state, login.nonce and login.codeVerifier (session or signed http-only cookie)
|
|
136
|
+
redirect(login.url);
|
|
137
|
+
|
|
138
|
+
// GET /callback
|
|
139
|
+
const { tokens, claims } = await auth.handleCallback(request.url, stored);
|
|
140
|
+
// checks error, state and iss, exchanges the code, verifies the ID token and its nonce
|
|
141
|
+
|
|
142
|
+
if (isLimited(claims)) {
|
|
143
|
+
// over your plan's user limit: anonymous sub "anon_...", no profile or wallet data
|
|
144
|
+
} else {
|
|
145
|
+
const user = await auth.userinfo(tokens.access_token); // x profile, wallets, onchain
|
|
146
|
+
}
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
| Method | |
|
|
150
|
+
|---|---|
|
|
151
|
+
| `discover()` | the discovery document (cached) |
|
|
152
|
+
| `createAuthorizationUrl({ scope?, prompt?, redirectUri?, state?, nonce? })` | returns `{ url, state, nonce, codeVerifier, redirectUri }` |
|
|
153
|
+
| `handleCallback(url, { state, nonce, codeVerifier })` | full callback handling, returns `{ tokens, claims }` |
|
|
154
|
+
| `exchangeCode({ code, codeVerifier, redirectUri? })` | `POST /oauth/token`, authorization_code |
|
|
155
|
+
| `refresh(refreshToken)` | `POST /oauth/token`, refresh_token. Tokens rotate: store the new one; reusing an old one revokes the chain |
|
|
156
|
+
| `revoke(token)` | `POST /oauth/revoke` |
|
|
157
|
+
| `userinfo(accessToken)` | `GET /oauth/userinfo` |
|
|
158
|
+
| `verifyIdToken(idToken, { nonce?, maxAge? })` | ES256 signature via JWKS, iss, aud, azp, exp, nonce |
|
|
159
|
+
| `verifyAccessToken(accessToken)` | for access tokens your frontend sends to your API (`typ: at+jwt`) |
|
|
160
|
+
|
|
161
|
+
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` (public clients). Access and ID tokens live 15 minutes; refresh tokens 30 days of inactivity, 90 days absolute.
|
|
162
|
+
|
|
163
|
+
**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. `isLimited(claims)` checks both.
|
|
164
|
+
|
|
165
|
+
A runnable server is in [examples/login-server.mjs](examples/login-server.mjs).
|
|
166
|
+
|
|
167
|
+
## Users without X
|
|
168
|
+
|
|
169
|
+
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:
|
|
170
|
+
|
|
171
|
+
| Field | With X | Without X |
|
|
172
|
+
|---|---|---|
|
|
173
|
+
| `preferred_username` (claims) / `username` | X handle, e.g. `alice` | `tg.<telegram name>` (Telegram) or `id.<hex>` (Google) — always contains a dot |
|
|
174
|
+
| `x` (userinfo, `/v1/profile`) | X profile / stats | `null` |
|
|
175
|
+
| `twitter` (profiles, trading profile blocks, leaderboards) | `https://x.com/<handle>` | `null` |
|
|
176
|
+
| `telegram` (profiles, trading profile blocks, leaderboards) | `https://t.me/<name>` or `""` | `https://t.me/<name>` or `""` |
|
|
177
|
+
| `name`, `picture` / `image_url` | from X | from Telegram or Google |
|
|
178
|
+
|
|
179
|
+
`profile_url` (`https://id.cabalspy.xyz/u/<handle>`) works for everyone. Thesis authors use the same handles.
|
|
180
|
+
|
|
181
|
+
```ts
|
|
182
|
+
import { hasX, handleKind } from 'cabalspy-id';
|
|
183
|
+
|
|
184
|
+
hasX(claims); // false for Telegram/Google sign-ins
|
|
185
|
+
handleKind('tg.alice'); // 'telegram'
|
|
186
|
+
const xLink = profile.twitter ?? null; // never build x.com links from tg./id. handles
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
## Data API
|
|
190
|
+
|
|
191
|
+
```ts
|
|
192
|
+
import { CabalSpyID } from 'cabalspy-id';
|
|
193
|
+
|
|
194
|
+
const csid = new CabalSpyID({ apiKey: process.env.CABALSPY_API_KEY });
|
|
195
|
+
|
|
196
|
+
const me = await csid.me(); // instance, plan, rate limit, users with data
|
|
197
|
+
|
|
198
|
+
// The person who just signed in: look them up by the `sub` of their ID token
|
|
199
|
+
const profile = await csid.profile({ csid_id: claims.sub });
|
|
200
|
+
const wallet = profile.wallets.find((w) => w.chains.includes('solana'))!;
|
|
201
|
+
|
|
202
|
+
// Full trading profile for a period, same numbers as the CabalSpy API
|
|
203
|
+
const t = await csid.tracker({ blockchain: 'solana', address: wallet.address, period: '7d' });
|
|
204
|
+
console.log(t.period_stats.realized_pnl_usd, t.period_win_rate_distribution.win_rate_percentage);
|
|
205
|
+
|
|
206
|
+
// Your users ranked by PnL
|
|
207
|
+
const board = await csid.leaderboard({ blockchain: 'solana', period: '7d', sort: 'pnl_usd', limit: 25 });
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
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.
|
|
211
|
+
|
|
212
|
+
| Method | Endpoint |
|
|
213
|
+
|---|---|
|
|
214
|
+
| `health()` | `GET /v1/health` (no key needed) |
|
|
215
|
+
| `me()` | `GET /v1/me` |
|
|
216
|
+
| `users({ limit?, kol?, cursor?, scope? })` | `GET /v1/users` |
|
|
217
|
+
| `profile({ username } \| { csid_id } \| { address })` | `GET /v1/profile` |
|
|
218
|
+
| `walletLookup(address)` | `GET /v1/wallets/lookup` |
|
|
219
|
+
| `tracker({ blockchain, address, period? })` | `GET /v1/wallets/tracker` |
|
|
220
|
+
| `history({ blockchain, address, limit?, cursor? })` | `GET /v1/wallets/history` |
|
|
221
|
+
| `holdings({ blockchain, address })` | `GET /v1/wallets/holdings` |
|
|
222
|
+
| `pnlCalendar({ blockchain, address })` | `GET /v1/wallets/pnl_calendar` |
|
|
223
|
+
| `leaderboard({ blockchain, period?, sort?, limit?, cursor?, scope? })` | `GET /v1/wallets/leaderboard` |
|
|
224
|
+
| `latestTransactions({ blockchain?, limit? })` | `GET /v1/transactions/latest` |
|
|
225
|
+
|
|
226
|
+
Chains: `solana`, `bnb`, `base`, `eth`, `rh`. Periods: `6h`, `1d`, `7d`, `30d`. Leaderboard sorts: `volume`, `pnl`, `pnl_usd`, `win_rate`, `trades`. Every response is fully typed.
|
|
227
|
+
|
|
228
|
+
If your account has several CabalSpy ID instances, pass `instance: 'csid_...'` (the client_id) to the constructor.
|
|
229
|
+
|
|
230
|
+
### Pagination
|
|
231
|
+
|
|
232
|
+
Async iterators follow `next_cursor` for you:
|
|
233
|
+
|
|
234
|
+
```ts
|
|
235
|
+
for await (const user of csid.iterUsers({ kol: true })) console.log(user.username);
|
|
236
|
+
for await (const trade of csid.iterHistory({ blockchain: 'solana', address })) console.log(trade.tx_signature);
|
|
237
|
+
for await (const entry of csid.iterLeaderboard({ blockchain: 'base', period: '30d' })) console.log(entry.rank);
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
For whole pages with metadata, use the `*Raw` methods (`usersRaw`, `historyRaw`, `leaderboardRaw`) with `csid.pages()`.
|
|
241
|
+
|
|
242
|
+
### Errors
|
|
243
|
+
|
|
244
|
+
Every failure is a typed subclass of `CabalSpyIDError` carrying `.code`, `.status`, `.requestId`, `.docs` (a link to the error's documentation), and for validation errors `.parameter` and `.allowed`.
|
|
245
|
+
|
|
246
|
+
```ts
|
|
247
|
+
import { NotFoundError, PlanUserLimitError, RateLimitError } from 'cabalspy-id';
|
|
248
|
+
|
|
249
|
+
try {
|
|
250
|
+
await csid.profile({ csid_id: claims.sub });
|
|
251
|
+
} catch (err) {
|
|
252
|
+
if (err instanceof PlanUserLimitError) return showUpgradeNotice(); // anon_ sub: no data until you upgrade
|
|
253
|
+
if (err instanceof NotFoundError) return null; // not one of your users
|
|
254
|
+
if (err instanceof RateLimitError) console.log(err.retryAfter);
|
|
255
|
+
throw err;
|
|
256
|
+
}
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
| Class | Status | Codes |
|
|
260
|
+
|---|---|---|
|
|
261
|
+
| `BadRequestError` | 400 | `missing_parameter`, `invalid_parameter`, `instance_required` |
|
|
262
|
+
| `AuthenticationError` | 401 | `missing_api_key` |
|
|
263
|
+
| `PermissionError` | 403 | `invalid_api_key`, `product_not_enabled`, `no_instance`, `scope_not_allowed` |
|
|
264
|
+
| `PlanUserLimitError` | 403 | `plan_user_limit` |
|
|
265
|
+
| `NotFoundError` | 404 | `profile_not_found`, `wallet_not_found`, `instance_not_found` |
|
|
266
|
+
| `RateLimitError` | 429 | `rate_limit_exceeded` |
|
|
267
|
+
| `ServerError` | 5xx | `internal_error`, `service_unavailable` |
|
|
268
|
+
| `ConnectionError` | – | network failure, timeout |
|
|
269
|
+
| `OAuthError` | 4xx | OAuth errors from the login endpoints (`invalid_grant`, `invalid_client`, …) |
|
|
270
|
+
| `TokenVerificationError` | – | ID / access token failed verification |
|
|
271
|
+
| `WebhookVerificationError` | – | webhook signature or timestamp invalid |
|
|
272
|
+
|
|
273
|
+
`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.lastRateLimit` (`X-RateLimit-Limit/Remaining/Reset`).
|
|
274
|
+
|
|
275
|
+
## Token theses
|
|
276
|
+
|
|
277
|
+
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.
|
|
278
|
+
|
|
279
|
+
Writing needs the scope **`thesis`** in your login (`scope: ['openid', 'profile', 'thesis']`).
|
|
280
|
+
|
|
281
|
+
**Scope of reads.** Theses are shared across platforms. Every read (Data API `listTheses`, `thesisSummary`, `latestTheses`; `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 use your `clientId`.
|
|
282
|
+
|
|
283
|
+
**Your public `clientId` is required** for everything that talks to the public thesis API (`ThesisClient`, `ThesisLive`, the theses widget). It is your instance's client_id (`csid_...`), shown in the dashboard next to the API key. It is public, not a secret and **not the API key** (the API key must never reach a browser). The SDK sends it as the `X-CSID-Client-Id` header on every call to `https://id.cabalspy.xyz/thesis/v1/...` and as `client_id` on the live socket URL (`wss://id.cabalspy.xyz/thesis/v1/ws/{chain}/{mint}?client_id=...`). Without it `new ThesisClient()` and `mountTheses()` / `<CabalSpyTheses>` throw an `AuthenticationError` with `code: 'client_id_required'`. Browser calls must come from one of the instance's allowed origins or verified domains (instances in dev mode also allow `http://localhost:*`). The Data API (`CabalSpyID`, API key) is unchanged.
|
|
284
|
+
|
|
285
|
+
### Widget
|
|
286
|
+
|
|
287
|
+
```tsx
|
|
288
|
+
import { CabalSpyTheses, useCabalSpyID } from 'cabalspy-id/react';
|
|
289
|
+
|
|
290
|
+
const { accessToken } = useCabalSpyID({ clientId: 'csid_...' });
|
|
291
|
+
<CabalSpyTheses chain="solana" mint={mint} clientId="csid_..." theme="dark" accessToken={accessToken} />
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
`clientId` (required) identifies your instance: the widget loads the design saved in the dashboard and credits posts to your app. Every design setting can be overridden by a prop (mapped to the widget's `data-*` attribute): `theme`, `scope`, `sort`, `font`, `radius`, `bg`, `card`, `text`, `muted`, `border`, `accent`, `accentText`, `bull`, `bear`, `showHeader`, `showSentiment`, `compact`, `height`.
|
|
295
|
+
|
|
296
|
+
Plain HTML: `<div data-csid-thesis data-chain="solana" data-mint="..." data-client-id="csid_..." data-theme="dark"></div>` with `widget.js`; `CSID.thesis.setAccessToken(token)` lets the signed-in person write without signing in again inside the widget (after a browser login through the widget this happens automatically).
|
|
297
|
+
|
|
298
|
+
**Your own sign-in.** If people sign in to your platform with CabalSpy ID already, let the widget's "Sign in to write" use it instead of the page's login button / popup: `setThesisSignInHandler(fn)` (from `cabalspy-id/widget`, or `CSID.thesis.setSignInHandler(fn)` in plain HTML) or the `onSignIn` prop of `<CabalSpyTheses>`. `fn` gets a thesis-capable access token for the person (for example your platform's CabalSpy ID access token with the scope `thesis`, from your backend), passes it on with `setThesisAccessToken(token)` and resolves `true`; it resolves `false` when the person is not signed in. While a handler is set, the widget treats your page as the source of truth for sign-in (signed out on the page = signed out in the widget). `setThesisSignInHandler(null)` removes it; `<CabalSpyTheses>` removes it on unmount.
|
|
299
|
+
|
|
300
|
+
```tsx
|
|
301
|
+
<CabalSpyTheses
|
|
302
|
+
chain="solana"
|
|
303
|
+
mint={mint}
|
|
304
|
+
clientId="csid_..."
|
|
305
|
+
onSignIn={async () => {
|
|
306
|
+
const token = await fetch('/api/csid-thesis-token').then((r) => (r.ok ? r.text() : null));
|
|
307
|
+
if (!token) return false;
|
|
308
|
+
await setThesisAccessToken(token);
|
|
309
|
+
return true;
|
|
310
|
+
}}
|
|
311
|
+
/>
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
### Your own UI in the browser: `ThesisClient`
|
|
315
|
+
|
|
316
|
+
The public thesis API (`https://id.cabalspy.xyz/thesis/v1`) needs no API key, only your public `clientId` (required). Reading needs nothing else; writing uses the signed-in user's access token (scope `thesis`).
|
|
317
|
+
|
|
318
|
+
```ts
|
|
319
|
+
import { ThesisClient } from 'cabalspy-id';
|
|
320
|
+
|
|
321
|
+
// clientId: your instance's PUBLIC client_id from the dashboard (not the API key)
|
|
322
|
+
const theses = new ThesisClient({ clientId: 'csid_...', accessToken }); // token from csid:login / useCabalSpyID()
|
|
323
|
+
// add scope: 'instance' to show only theses written on your platform
|
|
324
|
+
|
|
325
|
+
const { items, token, summary } = await theses.list('solana', mint, { sort: 'top' });
|
|
326
|
+
const me = await theses.me('solana', mint); // { can_post, can_reply, holder, paused, user }
|
|
327
|
+
if (me.can_post) await theses.post('solana', mint, { body: 'Strong community, still early.', stance: 'bull' });
|
|
328
|
+
await theses.reply(items[0].id, 'Agree, chart looks good');
|
|
329
|
+
await theses.react(items[0].id, '🚀');
|
|
330
|
+
|
|
331
|
+
const live = theses.live('solana', mint); // public websocket for one token, ?client_id=... (also takes { scope })
|
|
332
|
+
live.on('thesis', (t) => prepend(t)).on('reply', addReply).on('reaction', updateCounts).on('removed', ({ id }) => drop(id));
|
|
333
|
+
await live.connect();
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
Methods: `list`, `iterate`, `summary`, `get`, `replies`, `byUser`, `me`, `post`, `reply`, `react`, `report`, `delete`, `notifications`, `markNotificationsRead`, `appeal`, `live`, `setAccessToken`.
|
|
337
|
+
|
|
338
|
+
client_id errors use the SDK's error classes, so check `err.code` (docs: `https://docs.cabalspy.xyz/cabalspy-id/errors#<code>`):
|
|
339
|
+
|
|
340
|
+
| Code | HTTP | Live socket close | Error class | Meaning |
|
|
341
|
+
|---|---|---|---|---|
|
|
342
|
+
| `client_id_required` | 401 | 4401 | `AuthenticationError` | no client_id sent |
|
|
343
|
+
| `invalid_client` | 401 | 4401 | `AuthenticationError` | unknown or inactive client_id |
|
|
344
|
+
| `origin_not_allowed` | 403 | 4403 | `PermissionError` | the browser's Origin is not one of the instance's allowed origins / verified domains |
|
|
345
|
+
| `rate_limited` | 429 | 4429 | `RateLimitError` (`retryAfter: 60`) | too many requests per client or per IP |
|
|
346
|
+
|
|
347
|
+
`ThesisLive` does not reconnect after 4401 or 4403 (it emits `error` and stays closed); after 4429 it waits 60 s before reconnecting.
|
|
348
|
+
|
|
349
|
+
### Data API (server to server)
|
|
350
|
+
|
|
351
|
+
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.
|
|
352
|
+
|
|
353
|
+
```ts
|
|
354
|
+
const list = await csid.listTheses({ blockchain: 'solana', mint, sort: 'new' });
|
|
355
|
+
const counts = await csid.thesisSummary({ blockchain: 'solana', mint }); // theses, bull, bear, authors, token
|
|
356
|
+
|
|
357
|
+
const { can_post, reason } = await csid.thesisEligibility({ blockchain: 'solana', mint, csid_id: claims.sub });
|
|
358
|
+
if (can_post) {
|
|
359
|
+
const t = await csid.postThesis({ blockchain: 'solana', mint, body: 'Accumulating here.', stance: 'bull', csid_id: claims.sub });
|
|
360
|
+
}
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
| Method | Endpoint |
|
|
364
|
+
|---|---|
|
|
365
|
+
| `listTheses({ blockchain, mint, sort?, stance?, limit?, cursor?, csid_id? })` / `iterTheses()` | `GET /v1/theses` |
|
|
366
|
+
| `thesisSummary({ blockchain, mint })` | `GET /v1/theses/summary` |
|
|
367
|
+
| `thesis(id, { csid_id? })` | `GET /v1/theses/item` |
|
|
368
|
+
| `thesisReplies({ id, limit?, cursor? })` / `iterThesisReplies()` | `GET /v1/theses/replies` |
|
|
369
|
+
| `latestTheses({ blockchain?, users?: 'all' \| 'instance', limit?, cursor? })` | `GET /v1/theses/latest` |
|
|
370
|
+
| `userTheses({ csid_id } \| { username })` | `GET /v1/theses/user` |
|
|
371
|
+
| `thesisEligibility({ blockchain, mint, csid_id })` | `GET /v1/theses/eligibility` |
|
|
372
|
+
| `postThesis({ blockchain, mint, body, stance?, csid_id })` | `POST /v1/theses` |
|
|
373
|
+
| `replyToThesis({ thesis_id, body, csid_id })` | `POST /v1/theses/replies` |
|
|
374
|
+
| `reactToThesis({ thesis_id, emoji, on?, csid_id })` | `POST /v1/theses/reactions` |
|
|
375
|
+
| `reportThesis({ thesis_id, reason, note?, csid_id })` | `POST /v1/theses/report` |
|
|
376
|
+
| `deleteThesis({ thesis_id, csid_id })` | `POST /v1/theses/delete` |
|
|
377
|
+
|
|
378
|
+
| `thesisNotifications(csidId)` | `GET /v1/theses/notifications` |
|
|
379
|
+
| `appealThesisDecision(csidId, notificationId, message)` | `POST /v1/theses/appeal` |
|
|
380
|
+
|
|
381
|
+
Every thesis carries `reacted_by` (the latest reactions with who reacted) and, for theses, `latest_replies` (a preview of the newest replies).
|
|
382
|
+
|
|
383
|
+
**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: `thesisNotifications(csidId)` / `appealThesisDecision(...)`; in the browser: `ThesisClient.notifications()`, `markNotificationsRead()`, `appeal(id, message)`. `ThesisClient.me()` also returns `notices: { unread, latest, url }` so your UI can show a badge.
|
|
384
|
+
|
|
385
|
+
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.
|
|
386
|
+
|
|
387
|
+
### Theses stream (every plan)
|
|
388
|
+
|
|
389
|
+
Live theses for the tokens you choose, e.g. to put markers on a chart. Up to 200 tokens per connection (or all), available on every plan; subscriptions are sent again after every reconnect.
|
|
390
|
+
|
|
391
|
+
```ts
|
|
392
|
+
const ts = csid.thesesStream({ scope: 'global' }); // or 'instance'
|
|
393
|
+
ts.on('thesis', (t) => addMarker(t.blockchain, t.mint, t));
|
|
394
|
+
ts.on('thesis.removed', ({ id }) => removeMarker(id));
|
|
395
|
+
await ts.connect(); // resolves on `ready`
|
|
396
|
+
ts.subscribe([{ blockchain: 'solana', mint: MINT_A }, { blockchain: 'base', mint: TOKEN_B }]);
|
|
397
|
+
ts.unsubscribe([{ blockchain: 'base', mint: TOKEN_B }]);
|
|
398
|
+
// ts.subscribeAll() / ts.unsubscribeAll()
|
|
399
|
+
|
|
400
|
+
for await (const { event, data } of csid.thesesStream()) { … } // or iterate
|
|
401
|
+
```
|
|
402
|
+
|
|
403
|
+
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.
|
|
404
|
+
|
|
405
|
+
Also over the trade stream (Pro plan): `csid.stream({ events: ['theses'], blockchain: 'solana', 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` (`event.data`: `thesis_id`, `chain`, `mint`, `emoji`, `on`, `reactions`).
|
|
406
|
+
|
|
407
|
+
## Live stream
|
|
408
|
+
|
|
409
|
+
Every trade of your users the moment it is indexed, with the trader's profile (Pro plan and up).
|
|
410
|
+
|
|
411
|
+
```ts
|
|
412
|
+
const stream = csid.stream({ blockchain: 'solana' }); // omit blockchain for every chain
|
|
413
|
+
// events: ['trades'] (default), ['theses'] or both; mint: theses of one token
|
|
414
|
+
|
|
415
|
+
stream.on('trade', (t) => console.log(t.trader?.username, t.transaction_type, t.token_symbol, t.value_usd));
|
|
416
|
+
stream.on('error', (e) => console.error(e.code, e.message));
|
|
417
|
+
await stream.connect(); // resolves once the server confirmed the subscription
|
|
418
|
+
|
|
419
|
+
// or
|
|
420
|
+
for await (const trade of csid.stream()) console.log(trade);
|
|
421
|
+
```
|
|
422
|
+
|
|
423
|
+
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`.
|
|
424
|
+
|
|
425
|
+
Browsers, Deno, Bun and Node 22+ have a global `WebSocket`. On Node 18–21 install `ws` and pass it:
|
|
426
|
+
|
|
427
|
+
```ts
|
|
428
|
+
import { WebSocket } from 'ws';
|
|
429
|
+
const stream = csid.stream({ WebSocketImpl: WebSocket });
|
|
430
|
+
```
|
|
431
|
+
|
|
432
|
+
## Webhooks
|
|
433
|
+
|
|
434
|
+
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.
|
|
435
|
+
|
|
436
|
+
```ts
|
|
437
|
+
import { verifyWebhook } from 'cabalspy-id';
|
|
438
|
+
|
|
439
|
+
// pass the RAW body, exactly as received
|
|
440
|
+
const event = await verifyWebhook({ payload: rawBody, headers: req.headers, secret: process.env.CSID_WEBHOOK_SECRET! });
|
|
441
|
+
|
|
442
|
+
switch (event.type) {
|
|
443
|
+
case 'wallet.verified': /* event.data.sub, … */ break;
|
|
444
|
+
case 'consent.revoked': /* delete the user's data */ break;
|
|
445
|
+
}
|
|
446
|
+
```
|
|
447
|
+
|
|
448
|
+
Timestamps older than 5 minutes are rejected (`toleranceSeconds`). During a secret roll several signatures are sent; any match is accepted. Dedupe on `event.id`, it is the same on every retry. `signWebhook()` builds test requests for your receiver.
|
|
449
|
+
|
|
450
|
+
## Configuration
|
|
451
|
+
|
|
452
|
+
```ts
|
|
453
|
+
new CabalSpyID({
|
|
454
|
+
apiKey: process.env.CABALSPY_API_KEY, // env fallback: CABALSPY_ID_API_KEY, then CABALSPY_API_KEY
|
|
455
|
+
instance: undefined, // client_id, when the account has several instances
|
|
456
|
+
baseUrl: 'https://api-id.cabalspy.xyz/v1',
|
|
457
|
+
streamUrl: 'wss://api-id.cabalspy.xyz/v1/stream',
|
|
458
|
+
timeout: 30_000,
|
|
459
|
+
maxRetries: 2,
|
|
460
|
+
headers: {},
|
|
461
|
+
fetch: undefined, // custom fetch (proxies, tests)
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
new CabalSpyIDAuth({
|
|
465
|
+
clientId: 'csid_...',
|
|
466
|
+
clientSecret: undefined, // csid_sk_..., server apps only
|
|
467
|
+
redirectUri: undefined,
|
|
468
|
+
issuer: 'https://id.cabalspy.xyz',
|
|
469
|
+
tokenEndpointAuthMethod: undefined, // client_secret_basic | client_secret_post | none
|
|
470
|
+
clockTolerance: 60,
|
|
471
|
+
timeout: 15_000,
|
|
472
|
+
fetch: undefined,
|
|
473
|
+
});
|
|
474
|
+
```
|
|
475
|
+
|
|
476
|
+
Never ship the API key or the client secret to a browser. The Data API key and `CabalSpyIDAuth` with a secret belong on your server.
|
|
477
|
+
|
|
478
|
+
## Pricing
|
|
479
|
+
|
|
480
|
+
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.
|
|
481
|
+
|
|
482
|
+
| Plan | Price / month | Users with data | API rate | |
|
|
483
|
+
|---|---|---|---|---|
|
|
484
|
+
| Free | $0 | 50 | 1 req/s | |
|
|
485
|
+
| Starter | $49 | 200 | 5 req/s | |
|
|
486
|
+
| Pro | $149 | 500 | 10 req/s | + live stream |
|
|
487
|
+
| Business | $499 | 1,000 | 25 req/s | |
|
|
488
|
+
| Enterprise | custom | unlimited | custom + SLA | `scope=global` |
|
|
489
|
+
|
|
490
|
+
Yearly billing is 20% off. Card or crypto at [apidashboard.cabalspy.xyz/billing/csid](https://apidashboard.cabalspy.xyz/billing/csid).
|
|
491
|
+
|
|
492
|
+
## Docs
|
|
493
|
+
|
|
494
|
+
[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)
|
|
495
|
+
|
|
496
|
+
## Related
|
|
497
|
+
|
|
498
|
+
- [CabalSpy ID](https://id.cabalspy.xyz) · [leaderboard](https://id.cabalspy.xyz/leaderboard) · [API dashboard](https://apidashboard.cabalspy.xyz)
|
|
499
|
+
- CabalSpy ID SDKs: [TypeScript](https://www.npmjs.com/package/cabalspy-id) · [Python](https://pypi.org/project/cabalspy-id/)
|
|
500
|
+
- 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)
|
|
501
|
+
- [SDK overview](https://www.cabalspy.xyz/sdks/) · [MCP server](https://www.cabalspy.xyz/mcp/) · [CabalSpy Terminal](https://app.cabalspy.xyz/)
|
|
502
|
+
|
|
503
|
+
## License
|
|
504
|
+
|
|
505
|
+
MIT
|