robinhood-chain-sdk 0.8.0 → 0.8.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/README.md CHANGED
@@ -1,639 +1,645 @@
1
- # robinhood-chain-sdk
2
-
3
- [![npm version](https://img.shields.io/npm/v/robinhood-chain-sdk?style=flat-square)](https://www.npmjs.com/package/robinhood-chain-sdk)
4
- [![npm downloads](https://img.shields.io/npm/dm/robinhood-chain-sdk?style=flat-square)](https://www.npmjs.com/package/robinhood-chain-sdk)
5
- [![GitHub stars](https://img.shields.io/github/stars/madeonsol/robinhood-chain-sdk?style=flat-square&logo=github)](https://github.com/madeonsol/robinhood-chain-sdk)
6
- [![TypeScript](https://img.shields.io/badge/TypeScript-5.4+-blue?style=flat-square&logo=typescript&logoColor=white)](https://www.typescriptlang.org/)
7
- [![Zero Dependencies](https://img.shields.io/badge/dependencies-0-brightgreen?style=flat-square)](package.json)
8
- [![License: MIT](https://img.shields.io/badge/License-MIT-blue?style=flat-square)](LICENSE)
9
-
10
- > **Robinhood Chain API / SDK — EVM-native on-chain trading intelligence for Robinhood Chain (chain id 4663).** The official, fully-typed, zero-dependency TypeScript client for all 54 endpoints: live KOL trades and coordination, token discovery, batch reads & launch-bundle detection, the Uniswap DEX trade tape, liquidity removals, tokenized equities, 1-minute OHLC candles, deployer reputation with alerts and trajectories, smart-money wallet rankings, and four **push rule engines** (copy-trade, price alerts, KOL coordination, first touches) — served from a self-hosted Robinhood Chain node.
11
-
12
- > ⭐ **[Star on GitHub](https://github.com/madeonsol/robinhood-chain-sdk)** · 📂 **[Examples](./examples/)** · 🌐 **[Robinhood Chain](https://madeonsol.com/robinhood)** · 📚 **[API docs](https://madeonsol.com/api-docs)**
13
-
14
- Robinhood Chain (RHC) is an **Arbitrum Orbit L2, chain id 4663**. This SDK wraps the MadeOnSol Robinhood Chain API — every field is EVM-native (`token_address` lowercase `0x`, `eth_amount`, `tx_hash`, `block_number`, `net_flow_eth`). It runs in Node.js ≥ 18 and edge runtimes with **zero runtime dependencies** (native `fetch`; the WebSocket stream uses the optional `ws` package on Node < 22 and the platform WebSocket everywhere else).
15
-
16
- The KOL→EVM mapping is unique to MadeOnSol: each tracked Solana KOL's Robinhood-Chain wallet is recovered by tracing their Solana→EVM bridge deposits (deBridge / Relay / Mayan / Wormhole), then attributed on-chain to the effective trading account (`tx.from`, or the ERC-4337 userOp sender when the trade was bundled). Robinhood Chain coverage is **bundled into every MadeOnSol tier at no extra cost — same `msk_` API key, same base URL** as the Solana product.
17
-
18
- > **New in 0.8.0tokenized equities + the rug signal.** Two endpoints that were live on the API but had no SDK binding: `client.tokens.equities(params?)` → `GET /rhc/equities` (**BASIC**, typed `RhcEquitiesResponse` / `RhcEquity`) lists every official Robinhood tokenized stock/ETF (NVDA, SPY, AAPL, …) with live price / MC / liquidity and 24h trades / ETH volume / buyer-seller split. **Identity is the issuer BEACON, never the name** — a token is listed only if its contract is an EIP-1967 beacon proxy on Robinhood's issuer beacon, read from our own node; on ship day there were 20 fake "GameStop • Robinhood Token" contracts and 8 fake NVDAs with the exact official suffix, and none appear here. `client.lpEvents(params?)` → `GET /rhc/lp-events` (**PRO+**, typed `RhcLpEventsResponse` / `RhcLpEvent`) is the liquidity **removals** feed Uniswap v2/v3 `Burn` + v4 `ModifyLiquidity` with a negative delta on tracked pools, each row enriched with the token, the provider wallet, `provider_is_token_deployer` (the classic rug tell) and `provider_kol_name`. Removals ONLY: adds are not persisted, so an empty page means "no removals seen", never "no liquidity activity" the `coverage` block says `adds_persisted: false`. Amounts are raw uint256 strings; v4 rows carry `liquidity` only. Filter by `token` / `pool` / `provider` / `dex`, cursor via `next_before`. Data since 2026-08-05.
19
-
20
- > **New in 0.7.0`holder_growth`: who arrived and who left.** `client.tokens.holders(address)` (typed `RhcHoldersResponse`, new `RhcHolderGrowthWindow`) now returns `holder_growth` on `GET /rhc/tokens/{address}/holders`: `{ "1h", "24h", "7d" }` × `{ cutoff_block, entered, entered_still_holding, exited, net }`. *entered* = addresses whose first `Transfer` of the token landed at-or-after the window's cutoff block (any current balance); *entered_still_holding* = those still non-zero; *exited* = pre-existing holders whose last movement in the window left them at zero; *net* ≈ the change in `holder_count`. Pools and burn addresses are excluded from every count. This exists because RHC balances are folded from ERC-20 Transfer logs on our own node the fold keeps first-seen and last-moved blocks per address and retains zero-balance rows so it is a direct read, not an estimate; the Solana census is a point-in-time ledger scan with no history and cannot answer this. A window is `null` (never 0) only when the chain had no ingested trades in it; the whole block is `null` only if the growth read failed. Sanity check from ship day: a token launched that morning showed 593 entered / 560 still holding over 24h, and `holder_count` was exactly 560.
21
-
22
- > **New in 0.6.0 — wallet intelligence.** Ten new operations covering the Robinhood Chain wallet surface, which had no SDK binding at all until now: a new `client.wallet` namespace `profile()`, `pnl()`, `positions()`, `trades()`, `watchlist()`, `track()`, `untrack()`, `relabel()`, `trackedTrades()` and `trackedSummary()`. Everything is **ETH**-denominated, and cost basis is FIFO over a rolling 90-day window `cost_basis_observable_from` names the date the window opens, so a position opened before it reads as a sell with no matching buy. The profile / PnL / positions trio shares ONE snapshot cache server-side, so calling all three on an address costs roughly one computation rather than three; `cache_hit` says which call paid for it. Watchlist quotas are **per chain** (PRO 50 / ULTRA 100 / BUSINESS 500 RHC wallets), independent of your Solana list.
23
-
24
- ## New in 0.5.0 — stream fixes
25
-
26
- No REST changes; everything below is about `client.stream`.
27
-
28
- - **Channel names corrected.** `StreamChannel` now lists the six real RHC channels — `rhc:kol_trades`, `rhc:dex_trades` (the DEX firehose, ULTRA+), `rhc:copytrade:signals`, `rhc:price_alert:events`, `rhc:kol:coordination`, `rhc:kol:first_touches`. 0.4.0's `rhc:trades` never existed server-side; the server now accepts it as a deprecated alias of `rhc:dex_trades`, and the literal stays in the union marked `@deprecated` so 0.4.0 code keeps compiling.
29
- - **Event names corrected.** The firehose broadcasts `rhc:dex_trade` — a 0.4.0 `on("rhc:trade", …)` handler never fired, and is now a **compile error** so you find it. `StreamEventName` covers all six channels: `rhc:kol_trade`, `rhc:dex_trade`, `rhc:copytrade:signal`, `rhc:price_alert:dip` / `rhc:price_alert:recovery`, `rhc:kol:coordination`, `rhc:kol:first_touch`.
30
- - **Server warnings surfaced.** The server answers a bad subscribe (typo'd or tier-gated channel) with a `channels_rejected` warning frame — 0.4.0 silently dropped it, so the stream just looked healthy-but-quiet. It now emits a typed `"warning"` lifecycle event (`StreamWarning`: `code`, `rejected`, `valid_channels`, `message`).
31
-
32
- ## Quick start (10 seconds)
33
-
34
- ```bash
35
- npm install robinhood-chain-sdk
36
- ```
37
-
38
- ```ts
39
- import { RobinhoodClient } from "robinhood-chain-sdk";
40
-
41
- const client = new RobinhoodClient({ apiKey: "msk_..." }); // free key at madeonsol.com/pricing
42
-
43
- // Tokens being bought by 2+ tracked KOLs on Robinhood Chain right now
44
- const { tokens } = await client.kol.hotTokens({ window: "1h" });
45
- console.log(tokens[0]?.token_symbol, tokens[0]?.kols_buying, "KOLs, net", tokens[0]?.net_eth, "ETH");
46
- ```
47
-
48
- Requires **Node.js 18** (native `fetch`). Works in Cloudflare Workers, Vercel Edge, Bun, and Deno.
49
-
50
- ## Authentication
51
-
52
- Get a free API key at **[madeonsol.com/pricing](https://madeonsol.com/pricing)** keys start with `msk_`. The same key unlocks both the Solana API and Robinhood Chain.
53
-
54
- ```ts
55
- const client = new RobinhoodClient({
56
- apiKey: process.env.MADEONSOL_API_KEY!,
57
- maxRetries: 2, // optional — auto-retry on 429 / 5xx with backoff (default 2)
58
- });
59
- ```
60
-
61
- ## Every endpoint SDK method
62
-
63
- All 54 Robinhood Chain operations live under `https://madeonsol.com/api/v1`. Bearer `msk_` auth on every call. Everything is a `GET` except the two batch reads and the four rule engines at the bottom, which are full CRUD.
64
-
65
- | # | Endpoint | SDK method | Tier |
66
- |---|---|---|---|
67
- | 1 | `GET /rhc/kol/feed` | `client.kol.feed(params?)` | BASIC |
68
- | 2 | `GET /rhc/kol/leaderboard` | `client.kol.leaderboard(params?)` | BASIC |
69
- | 3 | `GET /rhc/kol/hot-tokens` | `client.kol.hotTokens(params?)` | BASIC |
70
- | 4 | `GET /rhc/kol/coordination` | `client.kol.coordination(params?)` | BASIC |
71
- | 5 | `GET /rhc/kol/first-touches` | `client.kol.firstTouches(params?)` | BASIC |
72
- | 6 | `GET /rhc/kol/{wallet}` | `client.kol.wallet(wallet)` | BASIC |
73
- | 7 | `GET /rhc/trades` | `client.trades(params?)` | PRO+ |
74
- | 8 | `GET /rhc/tokens` | `client.tokens.list(params?)` | PRO+ |
75
- | 9 | `GET /rhc/tokens/{address}` | `client.tokens.get(address)` | BASIC |
76
- | 10 | `GET /rhc/tokens/{address}/candles` | `client.tokens.candles(address, params?)` | PRO+ |
77
- | 11 | `GET /rhc/tokens/{address}/kol-consensus` | `client.tokens.kolConsensus(address)` | PRO+ |
78
- | 12 | `GET /rhc/tokens/{address}/buyer-quality` | `client.tokens.buyerQuality(address)` | BASIC |
79
- | 13 | `GET /rhc/tokens/{address}/bundle` | `client.tokens.bundle(address)` | BASIC |
80
- | 14 | `GET /rhc/tokens/{address}/top-traders` | `client.tokens.topTraders(address, params?)` | PRO+ |
81
- | 15 | `GET /rhc/tokens/{address}/flow` | `client.tokens.flow(address, window?)` | PRO+ |
82
- | 16 | `GET /rhc/tokens/{address}/peak-history` | `client.tokens.peakHistory(address, params?)` | PRO+ |
83
- | 17 | `GET /rhc/tokens/{address}/risk` | `client.tokens.risk(address)` | PRO+ |
84
- | 18 | `GET /rhc/tokens/{address}/holders` | `client.tokens.holders(address, params?)` | PRO+ |
85
- | 19 | `POST /rhc/token/batch` | `client.tokens.batch(addresses)` — max 50 | BASIC |
86
- | 20 | `POST /rhc/tokens/batch/buyer-quality` | `client.tokens.batchBuyerQuality(addresses)` — **max 20** | BASIC |
87
- | 21 | `GET /rhc/equities` | `client.tokens.equities(params?)` | BASIC |
88
- | 22 | `GET /rhc/lp-events` | `client.lpEvents(params?)` | PRO+ |
89
- | 23 | `GET /rhc/deployer-hunter/leaderboard` | `client.deployerHunter.leaderboard(params?)` | BASIC |
90
- | 24 | `GET /rhc/deployer-hunter/best-tokens` | `client.deployerHunter.bestTokens(params?)` | BASIC |
91
- | 25 | `GET /rhc/deployer-hunter/stats` | `client.deployerHunter.stats()` | BASIC |
92
- | 26 | `GET /rhc/deployer-hunter/alerts` | `client.deployerHunter.alerts(params?)` | BASIC |
93
- | 27 | `GET /rhc/deployer-hunter/recent-bonds` | `client.deployerHunter.recentBonds(params?)` | BASIC |
94
- | 28 | `GET /rhc/deployer-hunter/{address}` | `client.deployerHunter.profile(address)` | BASIC |
95
- | 29 | `GET /rhc/deployer-hunter/{address}/trajectory` | `client.deployerHunter.trajectory(address)` | BASIC |
96
- | 30 | `GET /rhc/deployer-hunter/{address}/tokens` | `client.deployerHunter.tokens(address, params?)` | BASIC |
97
- | 31 | `GET /rhc/deployer-hunter/{address}/history` | `client.deployerHunter.history(address, params?)` | PRO+ |
98
- | 32 | `GET /rhc/alpha-wallets` | `client.alphaWallets(params?)` | PRO+ |
99
- | 33 | `GET /rhc/copytrade/subscriptions` | `client.copyTrade.list()` | PRO+ |
100
- | 34 | `POST /rhc/copytrade/subscriptions` | `client.copyTrade.create(params)` | PRO+ |
101
- | 35 | `GET /rhc/copytrade/subscriptions/{id}` | `client.copyTrade.get(id)` | PRO+ |
102
- | 36 | `PATCH /rhc/copytrade/subscriptions/{id}` | `client.copyTrade.update(id, params)` | PRO+ |
103
- | 37 | `DELETE /rhc/copytrade/subscriptions/{id}` | `client.copyTrade.delete(id)` | PRO+ |
104
- | 38 | `GET /rhc/copytrade/signals` | `client.copyTrade.signals(params?)` | PRO+ |
105
- | 39 | `GET /rhc/price-alerts` | `client.priceAlerts.list()` | PRO+ |
106
- | 40 | `POST /rhc/price-alerts` | `client.priceAlerts.create(params)` | PRO+ |
107
- | 41 | `GET /rhc/price-alerts/{id}` | `client.priceAlerts.get(id)` | PRO+ |
108
- | 42 | `PATCH /rhc/price-alerts/{id}` | `client.priceAlerts.update(id, params)` | PRO+ |
109
- | 43 | `DELETE /rhc/price-alerts/{id}` | `client.priceAlerts.delete(id)` | PRO+ |
110
- | 44 | `GET /rhc/price-alerts/events` | `client.priceAlerts.events(params?)` | PRO+ |
111
- | 45 | `GET /rhc/kol/coordination/alerts` | `client.kol.coordinationAlerts.list()` | PRO+ |
112
- | 46 | `POST /rhc/kol/coordination/alerts` | `client.kol.coordinationAlerts.create(params)` | PRO+ |
113
- | 47 | `GET /rhc/kol/coordination/alerts/{id}` | `client.kol.coordinationAlerts.get(id)` | PRO+ |
114
- | 48 | `PATCH /rhc/kol/coordination/alerts/{id}` | `client.kol.coordinationAlerts.update(id, params)` | PRO+ |
115
- | 49 | `DELETE /rhc/kol/coordination/alerts/{id}` | `client.kol.coordinationAlerts.delete(id)` | PRO+ |
116
- | 50 | `GET /rhc/kol/first-touches/subscriptions` | `client.kol.firstTouchSubscriptions.list()` | ULTRA+ |
117
- | 51 | `POST /rhc/kol/first-touches/subscriptions` | `client.kol.firstTouchSubscriptions.create(params)` | ULTRA+ |
118
- | 52 | `GET /rhc/kol/first-touches/subscriptions/{id}` | `client.kol.firstTouchSubscriptions.get(id)` | ULTRA+ |
119
- | 53 | `PATCH /rhc/kol/first-touches/subscriptions/{id}` | `client.kol.firstTouchSubscriptions.update(id, params)` | ULTRA+ |
120
- | 54 | `DELETE /rhc/kol/first-touches/subscriptions/{id}` | `client.kol.firstTouchSubscriptions.delete(id)` | ULTRA+ |
121
- | + | `POST /stream/token` → WebSocket | `client.stream.connect()` | PRO+ |
122
-
123
- ## What you can build
124
-
125
- - **KOL copy-trading on Robinhood Chain** — stream `client.kol.feed()` / the `rhc:kol_trades` channel and mirror verified-KOL buys, EVM-native. Or stop polling entirely: `client.copyTrade.create()` has the server watch the tape and push you a signal.
126
- - **Push instead of poll** — four rule engines (`client.copyTrade`, `client.priceAlerts`, `client.kol.coordinationAlerts`, `client.kol.firstTouchSubscriptions`) deliver over webhook or WebSocket. **Quotas are per chain** — RHC rules never eat your Solana allowance.
127
- - **Consensus scanner** `client.kol.hotTokens()` surfaces tokens 2+ KOLs are accumulating; `client.kol.coordination()` adds the cohort composition behind it (per-KOL legs, accumulating vs distributing, exit state).
128
- - **Discovery bot** — `client.kol.firstTouches()` gives the globally earliest KOL buy per token, filterable to tokens minutes old.
129
- - **Rug watch** — `client.lpEvents({ token })` is the liquidity-removals feed (removals only, PRO+); flag `provider_is_token_deployer` rows the moment they land.
130
- - **Tokenized-equity screener** — `client.tokens.equities()` ranks every beacon-verified Robinhood stock/ETF token by 24h volume, trades or MC no name-matching, so the fake NVDA/GameStop contracts never show up.
131
- - **Launch-bundle / rug gate** — `client.tokens.bundle()` flags a same-block early-buyer bundle and how much of supply it still holds; `client.tokens.buyerQuality()` scores the first-20 cohort 0–100 with a dump-cluster ensemble.
132
- - **Portfolio / watchlist refresh** — `client.tokens.batch()` prices up to 50 tokens in one call, `client.tokens.batchBuyerQuality()` scores up to 20.
133
- - **MEV / sandwich analysis** — `client.trades()` gives every Uniswap v2/v3/v4 swap with the effective trading account (`trader_eoa`), `gas_price`, `tx_index`, and `method_selector`.
134
- - **Deployer due-diligence** — `client.deployerHunter.leaderboard()` / `.profile()` / `.trajectory()` / `.tokens()` rank and profile 40k+ RHC deployers; `.stats()` gives the chain-wide denominator.
135
- - **Deployer alert feed** — `client.deployerHunter.alerts()` pushes new deploys and graduations, tradability-filtered by default, with the tier resolved at read time.
136
- - **Smart-money discovery** — `client.alphaWallets()` ranks trader wallets by realized net ETH, win rate, and memecoin share, flagging bot fleets and known KOLs.
137
- - **Charting** — `client.tokens.candles()` returns 1-minute price + market-cap OHLC with buy/sell volume split.
138
-
139
- ## KOL trade intelligence — `client.kol`
140
-
141
- ### `client.kol.feed(params?)` `GET /rhc/kol/feed` (BASIC)
142
-
143
- Live buy/sell feed from tracked KOLs' verified Robinhood-Chain wallets, enriched with the token's current/peak MC, deployer tier, and `mc_multiple_since_trade` ("did the call run").
144
-
145
- ```ts
146
- const { trades, next_before } = await client.kol.feed({
147
- limit: 50, // 1–100
148
- action: "buy", // "buy" | "sell"
149
- kol: "0xabc…", // filter to one KOL's EVM wallet
150
- min_eth: 0.25, // minimum trade size in ETH
151
- // before: next_before, // cursor — page backwards
152
- });
153
- for (const t of trades) {
154
- console.log(t.kol_name, "bought", t.token_symbol, `${t.eth_amount} ETH`, `${t.mc_multiple_since_trade}x since`);
155
- }
156
- ```
157
-
158
- Returns `RhcKolFeedResponse` — `{ chain, trades: RhcKolFeedTrade[], count, data_age_seconds, next_before }`.
159
-
160
- ### `client.kol.leaderboard(params?)` — `GET /rhc/kol/leaderboard` (BASIC)
161
-
162
- KOLs ranked by trade count then net ETH flow over `24h` / `7d` / `30d`. `net_eth` is buy−sell flow (not realized PnL).
163
-
164
- ```ts
165
- const { leaderboard } = await client.kol.leaderboard({ period: "7d", limit: 25 });
166
- ```
167
-
168
- ### `client.kol.hotTokens(params?)` — `GET /rhc/kol/hot-tokens` (BASIC)
169
-
170
- Tokens bought by **2+ distinct KOLs** in the window (`5m`/`15m`/`1h`/`6h`/`24h`) — a consensus signal.
171
-
172
- ```ts
173
- const { tokens } = await client.kol.hotTokens({ window: "1h" });
174
- ```
175
-
176
- ### `client.kol.coordination(params?)` — `GET /rhc/kol/coordination` (BASIC)
177
-
178
- Tokens bought by **`min_kols`+ distinct KOLs** in the window, ranked by KOL count then buy volume. Deeper than `hotTokens()`: each row carries the per-KOL breakdown, `net_eth` (buys − sells in-window), an `accumulating` / `distributing` signal, `exited_count` vs `holders_count`, and `time_to_consensus_sec` (how fast the cohort piled in).
179
-
180
- ```ts
181
- const { coordination } = await client.kol.coordination({
182
- period: "24h", // "1h" | "6h" | "24h" | "7d"
183
- min_kols: 3, // 2–50
184
- limit: 20, // 1–50
185
- max_mc_usd: 250_000, // MC at the FIRST KOL buy (unknown entry MC is dropped when a band is set)
186
- });
187
- for (const c of coordination) {
188
- console.log(c.token_symbol, c.kol_count, "KOLs", c.signal, `${c.net_eth} ETH net`, `${c.holders_count} still holding`);
189
- }
190
- ```
191
-
192
- RHC has no KOL winrate/strategy tables, so the Solana `avg_winrate_7d` / `coordination_score` fields are intentionally absent.
193
-
194
- ### `client.kol.firstTouches(params?)` — `GET /rhc/kol/first-touches` (BASIC)
195
-
196
- The **globally earliest buy by any tracked KOL** per token the discovery signal. Each event carries the entry size in ETH, `tx_hash`, `token_age_minutes` at first touch, the MC at entry, and the current + peak MC so you can score how the call aged.
197
-
198
- ```ts
199
- const { events, next_before } = await client.kol.firstTouches({
200
- limit: 50, // 1–100clamped to 20 below PRO
201
- token_age_max_min: 60, // only tokens under an hour old at first touch
202
- min_eth: 0.1,
203
- // since: lastSeen, // poll forward
204
- // before: next_before, // page back
205
- });
206
- ```
207
-
208
- `first_kol.evm_address` is ULTRA-only; `name` and `twitter_url` are always returned.
209
-
210
- ### `client.kol.wallet(wallet)` — `GET /rhc/kol/{wallet}` (BASIC)
211
-
212
- Aggregate stats over one KOL's last 200 RHC trades plus their 50 most recent.
213
-
214
- ```ts
215
- const profile = await client.kol.wallet("0xabc…");
216
- console.log(profile.kol_name, profile.stats.net_eth, "ETH net");
217
- ```
218
-
219
- ## DEX trade tape — `client.trades(params?)` — `GET /rhc/trades` (PRO+)
220
-
221
- Every Uniswap v2/v3/v4 swap on chain 4663, ~sub-second from execution. Each row carries the effective trading account (`trader_eoa` — `tx.from` normally, or the ERC-4337 userOp sender when the trade was bundled; never the router or the bundler), gas/ordering for MEV work, pool state, and KOL/deployer flags. Cursor via `next_before`.
222
-
223
- ```ts
224
- const { trades } = await client.trades({
225
- token: "0xdef…", // filter to one token
226
- dex: "uniswap-v3", // "uniswap-v2" | "uniswap-v3" | "uniswap-v4"
227
- min_eth: 1,
228
- limit: 100,
229
- });
230
- for (const t of trades) {
231
- console.log(t.trader_eoa, t.action, t.eth_amount, "ETH", "gas", t.gas_price, "gwei", t.is_kol ? `(KOL ${t.kol_name})` : "");
232
- }
233
- ```
234
-
235
- ## Liquidity removals `client.lpEvents(params?)` `GET /rhc/lp-events` (PRO+)
236
-
237
- The rug signal. Uniswap v2/v3 `Burn` and v4 `ModifyLiquidity` with a negative delta on tracked pools, straight from our own node's log subscription — each row enriched with the token, the wallet that pulled (`provider`), `provider_is_token_deployer` (the classic rug shape), deployer tier and KOL name. Cursor via `next_before` (same opaque keyset as `client.trades()`).
238
-
239
- ```ts
240
- const { events, has_more, next_before, coverage } = await client.lpEvents({ token: "0xdef…", limit: 50 });
241
- for (const ev of events) {
242
- if (ev.provider_is_token_deployer) console.warn("deployer pulled liquidity:", ev.tx_hash, ev.dex, ev.token_amount_raw);
243
- }
244
- console.log(coverage.adds_persisted); // false always
245
- ```
246
-
247
- > **Removals ONLY.** Liquidity adds are not persisted (v4 adds share the topic and are dropped at decode; v2/v3 `Mint` is not subscribed), so every row is `event: "remove"` and an empty page means "no removals seen", never "no liquidity activity" — the `coverage` block spells this out. Amounts are **raw uint256 decimal strings** (`liquidity`, `amount0` / `amount1`, plus pre-resolved `token_amount_raw` / `quote_token` / `quote_amount_raw`) — do not `Number()` them; v4 rows carry `liquidity` only because the pool manager emits no token amounts. Filters: `token`, `pool` (v2/v3 address or v4 bytes32 poolId), `provider`, `dex`. Data since 2026-08-05.
248
-
249
- ## Token intelligence — `client.tokens`
250
-
251
- | Method | Endpoint | Tier | Returns |
252
- |---|---|---|---|
253
- | `list(params?)` | `/rhc/tokens` | PRO+ | Live-priced token discovery MC, liquidity, peak MC + drawdown, launchpad, deployer tier. Sort by `last_trade` / `market_cap` / `liquidity` / `peak_mc`. |
254
- | `equities(params?)` | `/rhc/equities` | BASIC | Every official Robinhood tokenized stock/ETF — **beacon-verified identity**, live price / MC / liquidity, 24h trades / ETH volume / buyers vs sellers. Sort by `volume` / `trades` / `market_cap` / `last_trade` / `symbol`; `symbol` (exact) / `q` (substring) filters. |
255
- | `get(address)` | `/rhc/tokens/{address}` | BASIC | Full snapshot: metadata, price/MC/FDV, peak + drawdown, deployer reputation block, KOL activity, pool inventory. |
256
- | `candles(address, params?)` | `/rhc/tokens/{address}/candles` | PRO+ | 1-minute price + market-cap OHLC, close liquidity, volume with buy/sell split, trade counts. |
257
- | `kolConsensus(address)` | `/rhc/tokens/{address}/kol-consensus` | PRO+ | KOL buyers vs sellers, `kol_exit_rate`, `net_flow_eth`, median entry MC, first touch. ULTRA adds buyer/exited wallet lists. |
258
- | `buyerQuality(address)` | `/rhc/tokens/{address}/buyer-quality` | BASIC | 0–100 first-20 buyer-cohort qualitywin-rate, KOL presence, bot-domination, bundle-buyer legs, dump-cluster ensemble. |
259
- | `bundle(address)` | `/rhc/tokens/{address}/bundle` | BASIC | Same-block launch-bundle detection + how much of what the cohort bought it still holds. |
260
- | `batch(addresses)` | `POST /rhc/token/batch` | BASIC | Up to **50** tokens in one call metadata, price/MC/FDV/liquidity, peak MC, deployer reputation. |
261
- | `batchBuyerQuality(addresses)` | `POST /rhc/tokens/batch/buyer-quality` | BASIC | Up to **20** tokens' early-buyer quality scores in one call. |
262
- | `topTraders(address, params?)` | `/rhc/tokens/{address}/top-traders` | PRO+ | Lifetime per-trader performance on one token, ranked by realized ETH, with win-rate / bot / KOL / dump-cluster enrichment. |
263
- | `flow(address, window?)` | `/rhc/tokens/{address}/flow` | PRO+ | Net buy/sell split by trader cohort who is accumulating and who is distributing. |
264
- | `peakHistory(address, params?)` | `/rhc/tokens/{address}/peak-history` | PRO+ | Peak MC, drawdown, and a running high-water curve. Returns both the recorded and the candle-derived observed peak. |
265
- | `risk(address)` | `/rhc/tokens/{address}/risk` | PRO+ | EVM-native risk computed **live**: proxy upgradeability, mint/pause capability, LP custody, and a live honeypot sell-simulation. |
266
- | `holders(address, params?)` | `/rhc/tokens/{address}/holders` | PRO+ | Exact holder set + concentration, folded from ERC-20 `Transfer` logs and reconciled against on-chain `totalSupply()`, plus `holder_growth` (1h/24h/7d entered / exited / net). |
267
-
268
- ### Tokenized stocks & ETFs — `equities(params?)` (BASIC)
269
-
270
- ```ts
271
- const { equities, total_equities, identity } = await client.tokens.equities({ sort: "volume", limit: 20 });
272
- for (const e of equities) {
273
- console.log(e.symbol, e.name, e.price_usd, "MC", e.market_cap_usd, "24h", e.trades_24h, "trades", e.volume_eth_24h, "ETH", `${e.buyers_24h}b/${e.sellers_24h}s`);
274
- }
275
- const nvda = await client.tokens.equities({ symbol: "NVDA" }); // exact ticker, case-insensitive
276
- console.log(identity.method, identity.issuer_beacon); // "beacon", 0xe10b…151b00
277
- ```
278
-
279
- > **Identity is the issuer BEACON, never the name.** A token is listed only if its contract is an EIP-1967 beacon proxy on Robinhood's issuer beacon, read from our own node every 10 minutes — `verified` is `true` by construction and `issuer_beacon` says which beacon. Name-matching would have listed 20 fake "GameStop • Robinhood Token" contracts and 8 fake NVDAs on ship day; none appear here. `name` has the "• Robinhood Token" suffix stripped for display; `onchain_name` is the raw ERC-20 name. 24h stats are cached 60 s (`stats_as_of`).
280
-
281
- ### Who is actually making money — `topTraders(address, params?)` (PRO+)
282
-
283
- ```ts
284
- const { traders } = await client.tokens.topTraders("0xdef…", { limit: 25 });
285
- for (const t of traders) {
286
- console.log(t.trader_eoa, t.net_eth, t.win_rate, t.likely_bot ? "(bot)" : "");
287
- }
288
- ```
289
-
290
- > **`net_eth` is REALIZED flow (`sell − buy`), not PnL.** It does not value a trader's remaining bag, so a wallet that bought and still holds ranks **last**, not first. For FIFO cost-basis PnL use `client.wallet.pnl()`.
291
-
292
- ### Who is buying vs dumping — `flow(address, window?)` (PRO+)
293
-
294
- ```ts
295
- const { cohorts } = await client.tokens.flow("0xdef…", "24h");
296
- // net_eth = sell buy, so POSITIVE means that cohort DISTRIBUTED.
297
- const bots = cohorts.find((c) => c.cohort === "bot");
298
- const smart = cohorts.find((c) => c.cohort === "smart_money");
299
- ```
300
-
301
- Cohorts are mutually exclusive, assigned by priority: `kol` → `bot` → `dump_cluster` → `early_buyer` → `unprofiled` → `smart_money` → `retail`. `smart_money` is derived (win-rate 0.5 and net positive), and `unprofiled` is a real answer — that trader simply has not met the reputation thresholds yet.
302
-
303
- ### How far off the top — `peakHistory(address, params?)` (PRO+)
304
-
305
- ```ts
306
- const p = await client.tokens.peakHistory("0xdef…", { window: "7d" });
307
- console.log(p.peak.drawdown_from_peak, p.peak.peak_mc_usd_recorded, p.peak.peak_mc_usd_observed);
308
- ```
309
-
310
- > **Two peaks are returned because they disagree.** `peak_mc_usd_recorded` is the stored high-water mark that deployer runner-rate and the $40K graduation bar key off; it is sampled from write batches, so it can undercount an intra-batch spike. `peak_mc_usd_observed` is the max of 1-minute candle highs — trade-level truth, and always ≥ recorded. Candle history begins 2026-07-15, so check `observed_covers_full_history` before treating the observed figure as a lifetime maximum.
311
-
312
- ### Can I actually sell this — `risk(address)` (PRO+)
313
-
314
- ```ts
315
- const r = await client.tokens.risk("0xdef…");
316
- if (r.sellability.sellable === "no") return; // bought-but-cannot-sell
317
- if (r.flags.includes("upgradeable") || r.capabilities.can_mint) { /* treat with care */ }
318
- ```
319
-
320
- > **This is not the Solana risk model.** EVM has no mint or freeze authority: across 300 random Robinhood Chain tokens only **2.3%** even expose an owner function and **0%** expose `mint` in their own bytecode — so an absent flag is the norm, **not** a safety signal. The signals that discriminate here are proxy upgradeability, LP custody and above all **sellability**, which is simulated at the chain head and never cached, because whether a token can be sold changes the instant an owner flips a setting. Note `owner.model: "none"` (no owner function at all) is a different answer from `"renounced"`.
321
-
322
- ### Who holds it — `holders(address, params?)` (PRO+)
323
-
324
- ```ts
325
- const h = await client.tokens.holders("0xdef…", { limit: 50 });
326
- if (!h.verified) console.warn("unverified:", h.unverified_reason);
327
- console.log(h.concentration?.top10_share, h.concentration?.pool_held_pct);
328
- const g = h.holder_growth?.["24h"];
329
- if (g) console.log(`24h: +${g.entered_still_holding} / -${g.exited} → net ${g.net}`);
330
- ```
331
-
332
- > Balances are folded from ERC-20 `Transfer` logs — **not** derived from trades — and reconciled against on-chain `totalSupply()` at a pinned block. **Check `verified` first**: `false` means the reconstruction is incomplete for that token and `unverified_reason` says why. Concentration **excludes liquidity pools and burn addresses** from the circulating denominator (the largest holder of a token is otherwise its own pool) and reports them separately as `pool_held_pct` / `burned_pct`. `balance` is a raw uint256 returned as a decimal **string** — do not `Number()` it. Holder addresses may be ERC-4337 smart accounts, so `holder_count` is not a headcount of people.
333
- >
334
- > **`holder_growth`** (`RhcHolderGrowthWindow`, keys `"1h"` / `"24h"` / `"7d"` + `note`) reports per window: `entered` (addresses whose first `Transfer` of the token landed at-or-after `cutoff_block`, any current balance), `entered_still_holding` (those still non-zero), `exited` (pre-existing holders whose last `Transfer` in the window left them at zero) and `net` = `entered_still_holding − exited` ≈ Δ `holder_count`. Pools and burns are excluded. A window is `null` only when the chain had no ingested trades in it; the whole object is `null` only if the growth read failed. This is possible because balances are folded from `Transfer` logs with history retained — the Solana census cannot answer it.
335
-
336
- ```ts
337
- // Launch-bundle + quality gate before buying
338
- const { bundle } = await client.tokens.bundle("0xdef…");
339
- const quality = await client.tokens.buyerQuality("0xdef…");
340
- if (bundle.bundle_kind === "same_block" && (bundle.held_pct_of_supply ?? 0) > 0.2 && !bundle.fully_exited) {
341
- // bundle still sitting on supply — it can dump
342
- }
343
- if (quality.quality.signal === "negative") { /* skip */ }
344
- ```
345
-
346
- > **EVM note:** Robinhood Chain is an Arbitrum Orbit L2 with no atomic multi-signer transaction, so a detected bundle is `bundle_kind: "same_block"` (or `"none"`) — there is no `atomic_tx` kind. KOL consensus is denominated in ETH (`net_flow_eth`).
347
-
348
- ### Batch reads
349
-
350
- ```ts
351
- // Up to 50 tokens, one round-trip. Set-based server-side, not a fan-out of get().
352
- // Every requested address is echoed back — unknown ones as { found: false } — so
353
- // positions line up with what you sent.
354
- const { tokens, requested, found } = await client.tokens.batch([token1, token2, token3]);
355
- for (const t of tokens) {
356
- if (t.found) console.log(t.symbol, t.market_cap_usd, t.deployer?.tier);
357
- }
358
-
359
- // Early-buyer quality for several tokens. MAX 20 — not the Solana batch cap of 50,
360
- // because each token is a per-token cohort computation (early-buyer scan + bundle
361
- // detection + alpha/cluster joins), so 50 would mean ~200 round-trips behind one
362
- // request. The cap comes back as `max_addresses`. A token that fails to score
363
- // degrades to an entry carrying `error` instead of failing the whole batch.
364
- const { tokens: scored } = await client.tokens.batchBuyerQuality([token1, token2]);
365
- for (const q of scored) {
366
- if ("error" in q) console.warn(q.token_address, q.error);
367
- else console.log(q.token_address, q.quality.score, q.quality.signal);
368
- }
369
- ```
370
-
371
- ## Deployer reputation — `client.deployerHunter`
372
-
373
- Most RHC launchpads are direct-to-DEX (no bonding curve), so "graduation" is a market-cap milestone: `graduation_rate` = share of a deployer's tokens that reached a **$40K+** peak MC; `runner_rate` = share that reached **$100K+**. `tier` is `elite` / `good` / `neutral` / `spammer`.
374
-
375
- > **Tier semantics (migrations 267 + 269).** `elite` / `good` are earned on the **$100K `runner_rate`** *and* require **24h of deployer history** the $40K bar proved farmable by operators mass-relaunching one ticker across rotating wallets, and a wallet minutes old can hit 5 launches on RHC. `graduation_rate` still means the $40K bar and is still returned everywhere, but it **no longer sets the tier**; `spammer` is the one label that still keys off it. Ranking by `graduation_rate` is ranking on a metric the tier ignores. `stats()` returns the thresholds actually in force.
376
-
377
- | Method | Endpoint | Tier | Returns |
378
- |---|---|---|---|
379
- | `leaderboard(params?)` | `/rhc/deployer-hunter/leaderboard` | BASIC | 40k+ deployers ranked over a 5-min-refresh rollup. |
380
- | `profile(address)` | `/rhc/deployer-hunter/{address}` | BASIC | Reputation row + 50 most recent tokens. |
381
- | `trajectory(address)` | `/rhc/deployer-hunter/{address}/trajectory` | BASIC | Getting better or worse — streaks, rolling 10-launch success curve, trend, cadence. |
382
- | `tokens(address, params?)` | `/rhc/deployer-hunter/{address}/tokens` | BASIC | Full paginated launch history with live + peak MC and liquidity. |
383
- | `history(address, params?)` | `/rhc/deployer-hunter/{address}/history` | PRO+ | Deploy history + reputation row, exact `total`, `graduated_pool`. |
384
- | `bestTokens(params?)` | `/rhc/deployer-hunter/best-tokens` | BASIC | Highest-peaking tokens from reputable (elite/good) deployers in a window. |
385
- | `stats()` | `/rhc/deployer-hunter/stats` | BASIC | Chain-wide summary population per tier, spam share, alert volume, active `tier_rules`. |
386
- | `alerts(params?)` | `/rhc/deployer-hunter/alerts` | BASIC | New-deploy / graduation signal feed, tradability-filtered, read-time tier. |
387
- | `recentBonds(params?)` | `/rhc/deployer-hunter/recent-bonds` | BASIC | Recent $40K graduations, newest peak first. |
388
-
389
- ```ts
390
- // Leaderboard 40k+ deployers, 5-min-refresh rollup
391
- const { deployers, has_more } = await client.deployerHunter.leaderboard({
392
- sort: "runner_rate", // graduation_rate | runner_rate | tokens_deployed | best_peak_mc_usd | last_deploy_at
393
- tier: "elite",
394
- min_tokens: 3,
395
- limit: 20,
396
- offset: 0,
397
- });
398
-
399
- // One deployer — unknown wallets return 200 with is_deployer:false (not a 404)
400
- const { is_deployer, deployer, recent_tokens } = await client.deployerHunter.profile("0xabc…");
401
- ```
402
-
403
- ### Is this deployer improving? `trajectory(address)` (BASIC)
404
-
405
- Current and longest hit/miss streaks, a rolling 10-launch success rate, best/worst stretches, average days between deploys, and how many launches they burn between a miss and the next hit.
406
-
407
- ```ts
408
- const { trajectory, success_metric, truncated } = await client.deployerHunter.trajectory("0xabc…");
409
- console.log(trajectory?.trend, trajectory?.current_streak, success_metric);
410
- ```
411
-
412
- The per-token success event here is the **$40K graduation** (echoed as `success_metric`), deliberately *not* the $100K runner bar that sets tiers — $100K is rare enough that most deployers would return an all-zero curve, and a trajectory needs events to have a shape. Analysis is capped at 500 launches; `truncated` tells you whether the curve is the whole story.
413
-
414
- ### Launch history — `tokens(address, params?)` (BASIC) and `history(address, params?)` (PRO+)
415
-
416
- ```ts
417
- // Enumerable launch history with live MC, peak MC and liquidity
418
- const { tokens, total, has_more, sort_scope } = await client.deployerHunter.tokens("0xabc…", {
419
- limit: 50, // 1–100
420
- offset: 0, // 0–10000
421
- sort: "first_seen_at", // "first_seen_at" | "peak_mc_usd"
422
- });
423
-
424
- // PRO+ — the same history with graduated_pool and an exact total
425
- const hist = await client.deployerHunter.history("0xabc…", { limit: 100, offset: 0 });
426
- ```
427
-
428
- `sort: "peak_mc_usd"` orders the fetched **page** only (the response echoes `sort_scope: "page"`), because peak MC lives in another table — it is not a global top-tokens ranking. Use `bestTokens()` for that. `profile()` caps `recent_tokens` at 50 and is a point-in-time read; `tokens()` is the enumerable list.
429
-
430
- ### Best tokens + chain stats
431
-
432
- ```ts
433
- // What did the deployers worth tracking actually produce?
434
- const { tokens, reputable_deployers, truncated } = await client.deployerHunter.bestTokens({
435
- period: "7d", // "24h" | "7d" | "30d" | "all"
436
- limit: 10, // 1–50
437
- });
438
-
439
- // The denominator for "is this deployer rare?"
440
- const stats = await client.deployerHunter.stats();
441
- console.log(stats.by_tier, stats.spam_token_share, stats.tier_rules.elite, stats.runner_definition);
442
- ```
443
-
444
- `bestTokens()` is gated on reputation rather than raw peak MC — the unfiltered version is `client.tokens.list({ sort: "peak_mc" })`. When `truncated` is true the top-N was drawn from the 1000 most *recent* launches in the period rather than the whole period.
445
-
446
- ### Deployer alerts — `alerts(params?)` (BASIC)
447
-
448
- New deploys and graduations from tracked deployers, newest first. Poll forward with `since: next_event_at`, page back with `before: next_before`. ULTRA gets the full limit; BASIC/PRO share a 50-alert cap.
449
-
450
- ```ts
451
- const { alerts, tradability_filter, next_event_at } = await client.deployerHunter.alerts({
452
- deployer_tier: "elite", // filters on the RESOLVED tier
453
- alert_type: "new_deploy", // "new_deploy" | "graduated"
454
- priority: "high", // "high" | "medium"
455
- min_mc: 10_000,
456
- limit: 50,
457
- // include_untradeable: true, // opt out of the liquidity gate
458
- });
459
- for (const a of alerts) {
460
- console.log(a.token_symbol, a.tier, a.liquidity_usd, a.tier_is_stale ? `(was ${a.tier_at_alert})` : "");
461
- }
462
- ```
463
-
464
- Two things worth knowing:
465
-
466
- - **Tradability is filtered by default.** Alerts on tokens with `liquidity_usd` below **$100** are dropped — unknown liquidity included, since on RHC that usually means a drained pool — because a $45K-MC alert on a $68 pool is not a signal. Pass `include_untradeable: true` for the raw tape; the active setting comes back as `tradability_filter`.
467
- - **`tier` is resolved at read time** from the live reputation view, so an alert can never advertise a reputation the deployer has since lost. The snapshot written when the alert fired is returned as `tier_at_alert`, with `tier_is_stale` flagging drift, and `deployer_tier=` filters on the resolved value so the filter and the payload always agree.
468
-
469
- ### Recent graduations — `recentBonds(params?)` (BASIC)
470
-
471
- ```ts
472
- const { tokens, graduation_mc } = await client.deployerHunter.recentBonds({
473
- deployer_tier: "good",
474
- min_peak: 100_000, // only raises the $40K floor, never lowers it
475
- limit: 50, // 1–200
476
- });
477
- ```
478
-
479
- On RHC a graduation is the **$40K peak-MC milestone**, not a bonding-curve completion — noxa/pons/clanker launch direct-to-DEX with no curve — so the set is defined purely by peak MC.
480
-
481
- ## Smart-money wallets — `client.alphaWallets(params?)` — `GET /rhc/alpha-wallets` (PRO+)
482
-
483
- The reverse of KOL discovery: rank Robinhood Chain trader wallets by realized on-chain performance. `net_eth` is realized net flow (sell − buy), `win_rate` is the share of tokens taken out profitably, `likely_bot` flags atomic-arb/MM fleets. RHC is dual-natured (launchpad memecoins vs tokenized stocks/stables), so filter with `min_memecoin_share` to isolate memecoin traders.
484
-
485
- ```ts
486
- const { wallets } = await client.alphaWallets({
487
- classification: "smart_money", // all | human | bot | smart_money
488
- identity: "unknown", // all | known_kol | unknown (net-new RHC smart money)
489
- min_memecoin_share: 0.7,
490
- sort: "net_eth", // net_eth | win_rate | trades | tokens | buy_eth | memecoin_share | last_trade_at
491
- limit: 25,
492
- });
493
- ```
494
-
495
- ## Rule engines — push, not polling
496
-
497
- Four server-side rule engines watch the Robinhood Chain tape for you and deliver over **webhook**, **WebSocket**, or both. **Every quota is per chain** — configuring RHC rules never consumes your Solana budget, and a full set of Solana rules leaves your RHC capacity untouched. A `webhook_secret` is returned **exactly once** on create (null when `delivery_mode` is `"websocket"`); payloads are signed HMAC-SHA256 over `` `<timestamp>.<body>` `` in the `X-MadeOnSol-Signature` header.
498
-
499
- ### Copy-trade`client.copyTrade` (PRO+)
500
-
501
- ```ts
502
- const { subscription, webhook_secret } = await client.copyTrade.create({
503
- name: "degen desk",
504
- source_wallets: ["0xaaa…", "0xbbb…", "0xccc…"], // 1–250, per-tier cap enforced server-side
505
- min_trade_eth: 0.01,
506
- only_action: "buy", // buy | sell | both
507
- sizing_mode: "fixed", // fixed | proportional | percent_source
508
- sizing_amount: 0.05, // ETH when sizing_mode is "fixed"
509
- delivery_mode: "websocket",
510
- });
511
-
512
- await client.copyTrade.update(subscription.id, { is_active: false });
513
- await client.copyTrade.delete(subscription.id); // fired signals cascade
514
-
515
- // Catch-up path for a missed webhook / dropped WS — fires retained 7 days
516
- const since = new Date(Date.now() - 3_600_000).toISOString();
517
- const { signals } = await client.copyTrade.signals({ subscription_id: subscription.id, since });
518
- ```
519
-
520
- Sizes are **ETH, not SOL**, and there is deliberately **no market-cap band** — the RHC trade event carries no market cap, so a band could only be a per-event DB lookup in the hot path of a ~3.3M-trades/day chain. `update()` re-checks the per-tier wallet cap, so a rule cannot be PATCHed past its limit.
521
-
522
- ### Price alerts — `client.priceAlerts` (PRO+)
523
-
524
- ```ts
525
- const { alert, evaluation } = await client.priceAlerts.create({
526
- token_address: "0xdef…", // must already be tracked on RHC with a market cap
527
- drop_pct: 30, // 0.01–99.99, measured from the MC captured RIGHT NOW
528
- recovery_pct: 15, // omit for a dip-only, terminal alert
529
- webhook_url: "https://example.com/hook",
530
- });
531
- console.log(evaluation.mode, evaluation.interval_seconds); // "polled", ~15
532
-
533
- const { events } = await client.priceAlerts.events({ alert_id: alert.id, event_type: "dip" });
534
- ```
535
-
536
- > **RHC price alerts are polled (~15s), not sub-second like the Solana ones.** `rhc_token_prices` is written by the RHC ingester on a separate box and emits no `pg_notify`, so there is nothing to react to — effective latency is that interval plus the token's own price-update cadence. Every create response spells this out in its `evaluation` block. The baseline MC is captured at creation, so an alert is a delta from the moment you set it; alerts self-expire after 30 days, and only `name`, `delivery_mode`, `webhook_url` and `is_active` are mutable (retuning a threshold mid-flight would make the recorded events uninterpretable).
537
-
538
- ### KOL coordination rules — `client.kol.coordinationAlerts` (PRO+)
539
-
540
- ```ts
541
- const { rule, scoring } = await client.kol.coordinationAlerts.create({
542
- min_kols: 3, // 2–50 distinct tracked KOL buyers
543
- window_minutes: 15, // 1–60 rolling window
544
- min_score: 40, // 0–100
545
- cooldown_min: 30, // 1–1440 before the same token can fire again
546
- score_jump_break: 20, // score jump that breaks the cooldown early
547
- delivery_mode: "websocket",
548
- });
549
- await client.kol.coordinationAlerts.update(rule.id, { min_kols: 4 }); // UUID id
550
- ```
551
-
552
- > **Coordination scoring is comparable to Solana, but not identical.** The shared v1 scorer runs, `quality` is a real KOL win-rate, and `earliness` is **defaulted** — RHC has no early-entry equivalent. The create response's `scoring` block records which components are real, and every fired signal repeats it in `score_inputs`.
553
-
554
- ### KOL first-touch subscriptions — `client.kol.firstTouchSubscriptions` (ULTRA+)
555
-
556
- ```ts
557
- const { subscription } = await client.kol.firstTouchSubscriptions.create({
558
- name: "early hands",
559
- filters: {
560
- min_first_buy_eth: 0.05,
561
- min_kol_winrate: 0.5, // win-rate on CLOSED positions
562
- strategy: "swing", // scalper | day_trader | swing | inactive | unscored
563
- min_mc_usd: 10_000,
564
- },
565
- delivery_mode: "websocket",
566
- });
567
-
568
- // `filters` is a whole-object REPLACE, not a merge — {} clears every filter
569
- await client.kol.firstTouchSubscriptions.update(subscription.id, { filters: {} });
570
- ```
571
-
572
- > **First-touch filters are not the Solana set.** RHC has no scout score, so `min_scout_tier` and `min_n_touches` do not exist here rather than silently matching nothing; `min_kol_winrate` and `strategy` are the quality gates. Unknown filter keys are rejected with a **400**, not ignored.
573
-
574
- ## Streaming — `client.stream` (PRO+)
575
-
576
- Managed WebSocket with token fetch + 24h refresh, auto-reconnect with backoff, heartbeat liveness, and typed events. Six RHC channels:
577
-
578
- | Channel | Emits | Tier | Scope |
579
- |---|---|---|---|
580
- | `rhc:kol_trades` | `rhc:kol_trade` | PRO+ | broadcast the live KOL tape |
581
- | `rhc:dex_trades` | `rhc:dex_trade` | **ULTRA+** | broadcast — the full DEX firehose |
582
- | `rhc:copytrade:signals` | `rhc:copytrade:signal` | PRO+ | user-scoped — only **your** rules' fires |
583
- | `rhc:price_alert:events` | `rhc:price_alert:dip`, `rhc:price_alert:recovery` | PRO+ | user-scoped; ~15s polled, not sub-second |
584
- | `rhc:kol:coordination` | `rhc:kol:coordination` | PRO+ | user-scopedonly **your** rules' fires |
585
- | `rhc:kol:first_touches` | `rhc:kol:first_touch` | PRO+ | broadcast — ULTRA gates only the first-touch *subscription CRUD*, not this channel |
586
-
587
- > **Deprecated:** `rhc:trades` was never a real channel — 0.4.0 subscribers got a `channels_rejected` warning and silence. The server now accepts it as an alias of `rhc:dex_trades` (and acks it under the canonical name), and the SDK keeps the literal marked `@deprecated` so 0.4.0 code compiles. Use `rhc:dex_trades`.
588
-
589
- ```ts
590
- const stream = client.stream.connect();
591
-
592
- stream
593
- .on("open", () => console.log("connected"))
594
- .on("rhc:kol_trade", (trade) => console.log("KOL trade", trade))
595
- .on("rhc:dex_trade", (trade) => console.log("DEX trade", trade))
596
- // New in 0.5.0 — the server tells you when a channel was refused (typo or
597
- // tier gate); 0.4.0 dropped this frame and the stream just stayed silent.
598
- .on("warning", (w) => console.warn("rejected:", w.code, w.rejected, w.valid_channels))
599
- .on("error", (err) => console.error(err));
600
-
601
- stream.subscribe(["rhc:kol_trades", "rhc:dex_trades"]);
602
- // …later
603
- stream.close(); // clean shutdown — short-lived scripts exit promptly
604
- ```
605
-
606
- On **Node < 22**, install the optional `ws` package (`npm i ws`) for the fastest clean exit; on Node ≥ 22 and in browsers the platform WebSocket is used automatically. You can also inject an implementation via `client.stream.connect({ WebSocketImpl })`.
607
-
608
- ## Error handling
609
-
610
- Every method throws `RobinhoodError` on a non-2xx response, with `.status`, `.body`, `.message`, and `.requestId` (the API's `_rid` include it when reporting issues). Rate-limits (`429`) and transient server errors (`5xx`) are retried automatically with exponential backoff, honoring `Retry-After` / `X-RateLimit-Reset`.
611
-
612
- ```ts
613
- import { RobinhoodError } from "robinhood-chain-sdk";
614
-
615
- try {
616
- await client.trades({ limit: 100 }); // PRO+
617
- } catch (err) {
618
- if (err instanceof RobinhoodError) {
619
- if (err.status === 403) console.error("Upgrade required:", err.message);
620
- else console.error(err.status, err.requestId, err.message);
621
- }
622
- }
623
- ```
624
-
625
- ## Types & constants
626
-
627
- Fully-typed responses and params for all 54 endpoints are exported (`RhcKolFeedResponse`, `RhcKolCoordinationResponse`, `RhcKolFirstTouchesResponse`, `RhcTradesResponse`, `RhcTokenSnapshot`, `RhcTokenBatchResponse`, `RhcBatchBuyerQualityResponse`, `RhcBundleResponse`, `RhcTopTradersResponse`, `RhcFlowResponse`, `RhcPeakHistoryResponse`, `RhcRiskResponse`, `RhcHoldersResponse`, `RhcHolderGrowthWindow`, `RhcEquitiesResponse`, `RhcEquity`, `RhcLpEventsResponse`, `RhcLpEvent`, `RhcDeployerTrajectoryResponse`, `RhcDeployerTokensResponse`, `RhcDeployerHistoryResponse`, `RhcBestTokensResponse`, `RhcDeployerStatsResponse`, `RhcDeployerAlertsResponse`, `RhcRecentBondsResponse`, `RhcAlphaWalletsResponse`, plus the rule engines: `RhcCopyTradeSubscription`, `RhcCopyTradeCreateParams`, `RhcCopyTradeSignal`, `RhcPriceAlert`, `RhcPriceAlertEvaluation`, `RhcPriceAlertEvent`, `RhcCoordinationAlertRule`, `RhcCoordinationAlertScoring`, `RhcFirstTouchSubscription`, `RhcFirstTouchFilters`, `RhcDeletedResponse`, …), plus shared types (`DeployerTier`, `TradeAction`, `UniswapVersion`, `DeliveryMode`, `RhcBundleKind`, `RhcAlertType`, `RhcAlertPriority`, `RhcCoordinationSignal`) and the `CHAIN_ID` constant (`4663`).
628
-
629
- ## Links
630
-
631
- - **Robinhood Chain** — [madeonsol.com/robinhood](https://madeonsol.com/robinhood)
632
- - **Pricing & free API key** — [madeonsol.com/pricing](https://madeonsol.com/pricing) (Robinhood Chain bundled into every tier)
633
- - **API docs** [madeonsol.com/api-docs](https://madeonsol.com/api-docs)
634
- - **npm** — [robinhood-chain-sdk](https://www.npmjs.com/package/robinhood-chain-sdk)
635
- - **GitHub** — [madeonsol/robinhood-chain-sdk](https://github.com/madeonsol/robinhood-chain-sdk)
636
-
637
- ## License
638
-
639
- MIT © MadeOnSol
1
+ # robinhood-chain-sdk
2
+
3
+ [![npm version](https://img.shields.io/npm/v/robinhood-chain-sdk?style=flat-square)](https://www.npmjs.com/package/robinhood-chain-sdk)
4
+ [![npm downloads](https://img.shields.io/npm/dm/robinhood-chain-sdk?style=flat-square)](https://www.npmjs.com/package/robinhood-chain-sdk)
5
+ [![GitHub stars](https://img.shields.io/github/stars/madeonsol/robinhood-chain-sdk?style=flat-square&logo=github)](https://github.com/madeonsol/robinhood-chain-sdk)
6
+ [![TypeScript](https://img.shields.io/badge/TypeScript-5.4+-blue?style=flat-square&logo=typescript&logoColor=white)](https://www.typescriptlang.org/)
7
+ [![Zero Dependencies](https://img.shields.io/badge/dependencies-0-brightgreen?style=flat-square)](package.json)
8
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue?style=flat-square)](LICENSE)
9
+
10
+ > **Robinhood Chain API / SDK — EVM-native on-chain trading intelligence for Robinhood Chain (chain id 4663).** The official, fully-typed, zero-dependency TypeScript client for all 54 endpoints: live KOL trades and coordination, token discovery, batch reads & launch-bundle detection, the Uniswap DEX trade tape, liquidity removals, tokenized equities, 1-minute OHLC candles, deployer reputation with alerts and trajectories, smart-money wallet rankings, and four **push rule engines** (copy-trade, price alerts, KOL coordination, first touches) — served from a self-hosted Robinhood Chain node.
11
+
12
+ > ⭐ **[Star on GitHub](https://github.com/madeonsol/robinhood-chain-sdk)** · 📂 **[Examples](./examples/)** · 🌐 **[Robinhood Chain](https://madeonsol.com/robinhood)** · 📚 **[API docs](https://madeonsol.com/api-docs)**
13
+
14
+ Robinhood Chain (RHC) is an **Arbitrum Orbit L2, chain id 4663**. This SDK wraps the MadeOnSol Robinhood Chain API — every field is EVM-native (`token_address` lowercase `0x`, `eth_amount`, `tx_hash`, `block_number`, `net_flow_eth`). It runs in Node.js ≥ 18 and edge runtimes with **zero runtime dependencies** (native `fetch`; the WebSocket stream uses the optional `ws` package on Node < 22 and the platform WebSocket everywhere else).
15
+
16
+ The KOL→EVM mapping is unique to MadeOnSol: each tracked Solana KOL's Robinhood-Chain wallet is recovered by tracing their Solana→EVM bridge deposits (deBridge / Relay / Mayan / Wormhole), then attributed on-chain to the effective trading account (`tx.from`, or the ERC-4337 userOp sender when the trade was bundled). Robinhood Chain coverage is **bundled into every MadeOnSol tier at no extra cost — same `msk_` API key, same base URL** as the Solana product.
17
+
18
+ > **New in 0.8.2mutation calls are no longer retried automatically (security fix, SDK-02).** A lost response or transient network error after a `POST`/`PATCH`/`DELETE` (rule create, watchlist change, `stream.rotate()`) used to retry automaticallywhich could duplicate a rule or rotate a token twice. Mutating calls (including the batch-read POST endpoints) now make exactly one attempt; `GET` retries/backoff (`maxRetries`) are unchanged. If a mutating call fails, check current state before deciding whether to retry by hand. No public API/type changes.
19
+ >
20
+ > **New in 0.8.1stream tokens never expire.** `POST /stream/token` now returns the **same token on every call, forever** (server change of 2026-08-27). `StreamToken.expires_at` is typed `string | null` and `next_refresh_at` `string | null` both are **always `null`** now and kept only for wire compatibility; the response gained `rotated: boolean` and `lifetime: string`. A token only stops working when the subscription lapses or you replace it with the new `client.stream.getToken({ rotate: true })` (the previous value keeps working for 60 s). The server never rotates on its own and never sends `token_refresh` unless you rotated; a `4001` close means "mint again", never a timer. Preferred handshake auth is `Authorization: Bearer <token>` (`?token=` still works and is masked in access logs); RHC channels ride the same socket and token as Solana. `client.stream.connect()` already fetched a token on every (re)connect and never read `expires_at`, so its behavior is unchanged only its docs are.
21
+
22
+ > **New in 0.8.0 — tokenized equities + the rug signal.** Two endpoints that were live on the API but had no SDK binding: `client.tokens.equities(params?)` `GET /rhc/equities` (**BASIC**, typed `RhcEquitiesResponse` / `RhcEquity`) lists every official Robinhood tokenized stock/ETF (NVDA, SPY, AAPL, …) with live price / MC / liquidity and 24h trades / ETH volume / buyer-seller split. **Identity is the issuer BEACON, never the name** — a token is listed only if its contract is an EIP-1967 beacon proxy on Robinhood's issuer beacon, read from our own node; on ship day there were 20 fake "GameStop • Robinhood Token" contracts and 8 fake NVDAs with the exact official suffix, and none appear here. `client.lpEvents(params?)` `GET /rhc/lp-events` (**PRO+**, typed `RhcLpEventsResponse` / `RhcLpEvent`) is the liquidity **removals** feed Uniswap v2/v3 `Burn` + v4 `ModifyLiquidity` with a negative delta on tracked pools, each row enriched with the token, the provider wallet, `provider_is_token_deployer` (the classic rug tell) and `provider_kol_name`. Removals ONLY: adds are not persisted, so an empty page means "no removals seen", never "no liquidity activity" the `coverage` block says `adds_persisted: false`. Amounts are raw uint256 strings; v4 rows carry `liquidity` only. Filter by `token` / `pool` / `provider` / `dex`, cursor via `next_before`. Data since 2026-08-05.
23
+
24
+ > **New in 0.7.0 — `holder_growth`: who arrived and who left.** `client.tokens.holders(address)` (typed `RhcHoldersResponse`, new `RhcHolderGrowthWindow`) now returns `holder_growth` on `GET /rhc/tokens/{address}/holders`: `{ "1h", "24h", "7d" }` × `{ cutoff_block, entered, entered_still_holding, exited, net }`. *entered* = addresses whose first `Transfer` of the token landed at-or-after the window's cutoff block (any current balance); *entered_still_holding* = those still non-zero; *exited* = pre-existing holders whose last movement in the window left them at zero; *net* ≈ the change in `holder_count`. Pools and burn addresses are excluded from every count. This exists because RHC balances are folded from ERC-20 Transfer logs on our own node — the fold keeps first-seen and last-moved blocks per address and retains zero-balance rows — so it is a direct read, not an estimate; the Solana census is a point-in-time ledger scan with no history and cannot answer this. A window is `null` (never 0) only when the chain had no ingested trades in it; the whole block is `null` only if the growth read failed. Sanity check from ship day: a token launched that morning showed 593 entered / 560 still holding over 24h, and `holder_count` was exactly 560.
25
+
26
+ > **New in 0.6.0 wallet intelligence.** Ten new operations covering the Robinhood Chain wallet surface, which had no SDK binding at all until now: a new `client.wallet` namespace — `profile()`, `pnl()`, `positions()`, `trades()`, `watchlist()`, `track()`, `untrack()`, `relabel()`, `trackedTrades()` and `trackedSummary()`. Everything is **ETH**-denominated, and cost basis is FIFO over a rolling 90-day window — `cost_basis_observable_from` names the date the window opens, so a position opened before it reads as a sell with no matching buy. The profile / PnL / positions trio shares ONE snapshot cache server-side, so calling all three on an address costs roughly one computation rather than three; `cache_hit` says which call paid for it. Watchlist quotas are **per chain** (PRO 50 / ULTRA 100 / BUSINESS 500 RHC wallets), independent of your Solana list.
27
+
28
+ ## New in 0.5.0 stream fixes
29
+
30
+ No REST changes; everything below is about `client.stream`.
31
+
32
+ - **Channel names corrected.** `StreamChannel` now lists the six real RHC channels — `rhc:kol_trades`, `rhc:dex_trades` (the DEX firehose, ULTRA+), `rhc:copytrade:signals`, `rhc:price_alert:events`, `rhc:kol:coordination`, `rhc:kol:first_touches`. 0.4.0's `rhc:trades` never existed server-side; the server now accepts it as a deprecated alias of `rhc:dex_trades`, and the literal stays in the union marked `@deprecated` so 0.4.0 code keeps compiling.
33
+ - **Event names corrected.** The firehose broadcasts `rhc:dex_trade` — a 0.4.0 `on("rhc:trade", …)` handler never fired, and is now a **compile error** so you find it. `StreamEventName` covers all six channels: `rhc:kol_trade`, `rhc:dex_trade`, `rhc:copytrade:signal`, `rhc:price_alert:dip` / `rhc:price_alert:recovery`, `rhc:kol:coordination`, `rhc:kol:first_touch`.
34
+ - **Server warnings surfaced.** The server answers a bad subscribe (typo'd or tier-gated channel) with a `channels_rejected` warning frame — 0.4.0 silently dropped it, so the stream just looked healthy-but-quiet. It now emits a typed `"warning"` lifecycle event (`StreamWarning`: `code`, `rejected`, `valid_channels`, `message`).
35
+
36
+ ## Quick start (10 seconds)
37
+
38
+ ```bash
39
+ npm install robinhood-chain-sdk
40
+ ```
41
+
42
+ ```ts
43
+ import { RobinhoodClient } from "robinhood-chain-sdk";
44
+
45
+ const client = new RobinhoodClient({ apiKey: "msk_..." }); // free key at madeonsol.com/pricing
46
+
47
+ // Tokens being bought by 2+ tracked KOLs on Robinhood Chain right now
48
+ const { tokens } = await client.kol.hotTokens({ window: "1h" });
49
+ console.log(tokens[0]?.token_symbol, tokens[0]?.kols_buying, "KOLs, net", tokens[0]?.net_eth, "ETH");
50
+ ```
51
+
52
+ Requires **Node.js 18** (native `fetch`). Works in Cloudflare Workers, Vercel Edge, Bun, and Deno.
53
+
54
+ ## Authentication
55
+
56
+ Get a free API key at **[madeonsol.com/pricing](https://madeonsol.com/pricing)** — keys start with `msk_`. The same key unlocks both the Solana API and Robinhood Chain.
57
+
58
+ ```ts
59
+ const client = new RobinhoodClient({
60
+ apiKey: process.env.MADEONSOL_API_KEY!,
61
+ maxRetries: 2, // optional GET retries on network errors / 429 / 5xx (default 2)
62
+ });
63
+ ```
64
+
65
+ ## Every endpoint SDK method
66
+
67
+ All 54 Robinhood Chain operations live under `https://madeonsol.com/api/v1`. Bearer `msk_` auth on every call. Everything is a `GET` except the two batch reads and the four rule engines at the bottom, which are full CRUD.
68
+
69
+ | # | Endpoint | SDK method | Tier |
70
+ |---|---|---|---|
71
+ | 1 | `GET /rhc/kol/feed` | `client.kol.feed(params?)` | BASIC |
72
+ | 2 | `GET /rhc/kol/leaderboard` | `client.kol.leaderboard(params?)` | BASIC |
73
+ | 3 | `GET /rhc/kol/hot-tokens` | `client.kol.hotTokens(params?)` | BASIC |
74
+ | 4 | `GET /rhc/kol/coordination` | `client.kol.coordination(params?)` | BASIC |
75
+ | 5 | `GET /rhc/kol/first-touches` | `client.kol.firstTouches(params?)` | BASIC |
76
+ | 6 | `GET /rhc/kol/{wallet}` | `client.kol.wallet(wallet)` | BASIC |
77
+ | 7 | `GET /rhc/trades` | `client.trades(params?)` | PRO+ |
78
+ | 8 | `GET /rhc/tokens` | `client.tokens.list(params?)` | PRO+ |
79
+ | 9 | `GET /rhc/tokens/{address}` | `client.tokens.get(address)` | BASIC |
80
+ | 10 | `GET /rhc/tokens/{address}/candles` | `client.tokens.candles(address, params?)` | PRO+ |
81
+ | 11 | `GET /rhc/tokens/{address}/kol-consensus` | `client.tokens.kolConsensus(address)` | PRO+ |
82
+ | 12 | `GET /rhc/tokens/{address}/buyer-quality` | `client.tokens.buyerQuality(address)` | BASIC |
83
+ | 13 | `GET /rhc/tokens/{address}/bundle` | `client.tokens.bundle(address)` | BASIC |
84
+ | 14 | `GET /rhc/tokens/{address}/top-traders` | `client.tokens.topTraders(address, params?)` | PRO+ |
85
+ | 15 | `GET /rhc/tokens/{address}/flow` | `client.tokens.flow(address, window?)` | PRO+ |
86
+ | 16 | `GET /rhc/tokens/{address}/peak-history` | `client.tokens.peakHistory(address, params?)` | PRO+ |
87
+ | 17 | `GET /rhc/tokens/{address}/risk` | `client.tokens.risk(address)` | PRO+ |
88
+ | 18 | `GET /rhc/tokens/{address}/holders` | `client.tokens.holders(address, params?)` | PRO+ |
89
+ | 19 | `POST /rhc/token/batch` | `client.tokens.batch(addresses)` — max 50 | BASIC |
90
+ | 20 | `POST /rhc/tokens/batch/buyer-quality` | `client.tokens.batchBuyerQuality(addresses)` — **max 20** | BASIC |
91
+ | 21 | `GET /rhc/equities` | `client.tokens.equities(params?)` | BASIC |
92
+ | 22 | `GET /rhc/lp-events` | `client.lpEvents(params?)` | PRO+ |
93
+ | 23 | `GET /rhc/deployer-hunter/leaderboard` | `client.deployerHunter.leaderboard(params?)` | BASIC |
94
+ | 24 | `GET /rhc/deployer-hunter/best-tokens` | `client.deployerHunter.bestTokens(params?)` | BASIC |
95
+ | 25 | `GET /rhc/deployer-hunter/stats` | `client.deployerHunter.stats()` | BASIC |
96
+ | 26 | `GET /rhc/deployer-hunter/alerts` | `client.deployerHunter.alerts(params?)` | BASIC |
97
+ | 27 | `GET /rhc/deployer-hunter/recent-bonds` | `client.deployerHunter.recentBonds(params?)` | BASIC |
98
+ | 28 | `GET /rhc/deployer-hunter/{address}` | `client.deployerHunter.profile(address)` | BASIC |
99
+ | 29 | `GET /rhc/deployer-hunter/{address}/trajectory` | `client.deployerHunter.trajectory(address)` | BASIC |
100
+ | 30 | `GET /rhc/deployer-hunter/{address}/tokens` | `client.deployerHunter.tokens(address, params?)` | BASIC |
101
+ | 31 | `GET /rhc/deployer-hunter/{address}/history` | `client.deployerHunter.history(address, params?)` | PRO+ |
102
+ | 32 | `GET /rhc/alpha-wallets` | `client.alphaWallets(params?)` | PRO+ |
103
+ | 33 | `GET /rhc/copytrade/subscriptions` | `client.copyTrade.list()` | PRO+ |
104
+ | 34 | `POST /rhc/copytrade/subscriptions` | `client.copyTrade.create(params)` | PRO+ |
105
+ | 35 | `GET /rhc/copytrade/subscriptions/{id}` | `client.copyTrade.get(id)` | PRO+ |
106
+ | 36 | `PATCH /rhc/copytrade/subscriptions/{id}` | `client.copyTrade.update(id, params)` | PRO+ |
107
+ | 37 | `DELETE /rhc/copytrade/subscriptions/{id}` | `client.copyTrade.delete(id)` | PRO+ |
108
+ | 38 | `GET /rhc/copytrade/signals` | `client.copyTrade.signals(params?)` | PRO+ |
109
+ | 39 | `GET /rhc/price-alerts` | `client.priceAlerts.list()` | PRO+ |
110
+ | 40 | `POST /rhc/price-alerts` | `client.priceAlerts.create(params)` | PRO+ |
111
+ | 41 | `GET /rhc/price-alerts/{id}` | `client.priceAlerts.get(id)` | PRO+ |
112
+ | 42 | `PATCH /rhc/price-alerts/{id}` | `client.priceAlerts.update(id, params)` | PRO+ |
113
+ | 43 | `DELETE /rhc/price-alerts/{id}` | `client.priceAlerts.delete(id)` | PRO+ |
114
+ | 44 | `GET /rhc/price-alerts/events` | `client.priceAlerts.events(params?)` | PRO+ |
115
+ | 45 | `GET /rhc/kol/coordination/alerts` | `client.kol.coordinationAlerts.list()` | PRO+ |
116
+ | 46 | `POST /rhc/kol/coordination/alerts` | `client.kol.coordinationAlerts.create(params)` | PRO+ |
117
+ | 47 | `GET /rhc/kol/coordination/alerts/{id}` | `client.kol.coordinationAlerts.get(id)` | PRO+ |
118
+ | 48 | `PATCH /rhc/kol/coordination/alerts/{id}` | `client.kol.coordinationAlerts.update(id, params)` | PRO+ |
119
+ | 49 | `DELETE /rhc/kol/coordination/alerts/{id}` | `client.kol.coordinationAlerts.delete(id)` | PRO+ |
120
+ | 50 | `GET /rhc/kol/first-touches/subscriptions` | `client.kol.firstTouchSubscriptions.list()` | ULTRA+ |
121
+ | 51 | `POST /rhc/kol/first-touches/subscriptions` | `client.kol.firstTouchSubscriptions.create(params)` | ULTRA+ |
122
+ | 52 | `GET /rhc/kol/first-touches/subscriptions/{id}` | `client.kol.firstTouchSubscriptions.get(id)` | ULTRA+ |
123
+ | 53 | `PATCH /rhc/kol/first-touches/subscriptions/{id}` | `client.kol.firstTouchSubscriptions.update(id, params)` | ULTRA+ |
124
+ | 54 | `DELETE /rhc/kol/first-touches/subscriptions/{id}` | `client.kol.firstTouchSubscriptions.delete(id)` | ULTRA+ |
125
+ | + | `POST /stream/token` WebSocket | `client.stream.connect()` | PRO+ |
126
+
127
+ ## What you can build
128
+
129
+ - **KOL copy-trading on Robinhood Chain** — stream `client.kol.feed()` / the `rhc:kol_trades` channel and mirror verified-KOL buys, EVM-native. Or stop polling entirely: `client.copyTrade.create()` has the server watch the tape and push you a signal.
130
+ - **Push instead of poll** — four rule engines (`client.copyTrade`, `client.priceAlerts`, `client.kol.coordinationAlerts`, `client.kol.firstTouchSubscriptions`) deliver over webhook or WebSocket. **Quotas are per chain**RHC rules never eat your Solana allowance.
131
+ - **Consensus scanner** — `client.kol.hotTokens()` surfaces tokens 2+ KOLs are accumulating; `client.kol.coordination()` adds the cohort composition behind it (per-KOL legs, accumulating vs distributing, exit state).
132
+ - **Discovery bot** — `client.kol.firstTouches()` gives the globally earliest KOL buy per token, filterable to tokens minutes old.
133
+ - **Rug watch** — `client.lpEvents({ token })` is the liquidity-removals feed (removals only, PRO+); flag `provider_is_token_deployer` rows the moment they land.
134
+ - **Tokenized-equity screener** — `client.tokens.equities()` ranks every beacon-verified Robinhood stock/ETF token by 24h volume, trades or MC no name-matching, so the fake NVDA/GameStop contracts never show up.
135
+ - **Launch-bundle / rug gate** — `client.tokens.bundle()` flags a same-block early-buyer bundle and how much of supply it still holds; `client.tokens.buyerQuality()` scores the first-20 cohort 0–100 with a dump-cluster ensemble.
136
+ - **Portfolio / watchlist refresh** — `client.tokens.batch()` prices up to 50 tokens in one call, `client.tokens.batchBuyerQuality()` scores up to 20.
137
+ - **MEV / sandwich analysis** — `client.trades()` gives every Uniswap v2/v3/v4 swap with the effective trading account (`trader_eoa`), `gas_price`, `tx_index`, and `method_selector`.
138
+ - **Deployer due-diligence** — `client.deployerHunter.leaderboard()` / `.profile()` / `.trajectory()` / `.tokens()` rank and profile 40k+ RHC deployers; `.stats()` gives the chain-wide denominator.
139
+ - **Deployer alert feed** — `client.deployerHunter.alerts()` pushes new deploys and graduations, tradability-filtered by default, with the tier resolved at read time.
140
+ - **Smart-money discovery** — `client.alphaWallets()` ranks trader wallets by realized net ETH, win rate, and memecoin share, flagging bot fleets and known KOLs.
141
+ - **Charting** — `client.tokens.candles()` returns 1-minute price + market-cap OHLC with buy/sell volume split.
142
+
143
+ ## KOL trade intelligence `client.kol`
144
+
145
+ ### `client.kol.feed(params?)` — `GET /rhc/kol/feed` (BASIC)
146
+
147
+ Live buy/sell feed from tracked KOLs' verified Robinhood-Chain wallets, enriched with the token's current/peak MC, deployer tier, and `mc_multiple_since_trade` ("did the call run").
148
+
149
+ ```ts
150
+ const { trades, next_before } = await client.kol.feed({
151
+ limit: 50, // 1–100
152
+ action: "buy", // "buy" | "sell"
153
+ kol: "0xabc…", // filter to one KOL's EVM wallet
154
+ min_eth: 0.25, // minimum trade size in ETH
155
+ // before: next_before, // cursor — page backwards
156
+ });
157
+ for (const t of trades) {
158
+ console.log(t.kol_name, "bought", t.token_symbol, `${t.eth_amount} ETH`, `${t.mc_multiple_since_trade}x since`);
159
+ }
160
+ ```
161
+
162
+ Returns `RhcKolFeedResponse` `{ chain, trades: RhcKolFeedTrade[], count, data_age_seconds, next_before }`.
163
+
164
+ ### `client.kol.leaderboard(params?)` — `GET /rhc/kol/leaderboard` (BASIC)
165
+
166
+ KOLs ranked by trade count then net ETH flow over `24h` / `7d` / `30d`. `net_eth` is buy−sell flow (not realized PnL).
167
+
168
+ ```ts
169
+ const { leaderboard } = await client.kol.leaderboard({ period: "7d", limit: 25 });
170
+ ```
171
+
172
+ ### `client.kol.hotTokens(params?)` — `GET /rhc/kol/hot-tokens` (BASIC)
173
+
174
+ Tokens bought by **2+ distinct KOLs** in the window (`5m`/`15m`/`1h`/`6h`/`24h`) — a consensus signal.
175
+
176
+ ```ts
177
+ const { tokens } = await client.kol.hotTokens({ window: "1h" });
178
+ ```
179
+
180
+ ### `client.kol.coordination(params?)` — `GET /rhc/kol/coordination` (BASIC)
181
+
182
+ Tokens bought by **`min_kols`+ distinct KOLs** in the window, ranked by KOL count then buy volume. Deeper than `hotTokens()`: each row carries the per-KOL breakdown, `net_eth` (buys − sells in-window), an `accumulating` / `distributing` signal, `exited_count` vs `holders_count`, and `time_to_consensus_sec` (how fast the cohort piled in).
183
+
184
+ ```ts
185
+ const { coordination } = await client.kol.coordination({
186
+ period: "24h", // "1h" | "6h" | "24h" | "7d"
187
+ min_kols: 3, // 2–50
188
+ limit: 20, // 1–50
189
+ max_mc_usd: 250_000, // MC at the FIRST KOL buy (unknown entry MC is dropped when a band is set)
190
+ });
191
+ for (const c of coordination) {
192
+ console.log(c.token_symbol, c.kol_count, "KOLs", c.signal, `${c.net_eth} ETH net`, `${c.holders_count} still holding`);
193
+ }
194
+ ```
195
+
196
+ RHC has no KOL winrate/strategy tables, so the Solana `avg_winrate_7d` / `coordination_score` fields are intentionally absent.
197
+
198
+ ### `client.kol.firstTouches(params?)` — `GET /rhc/kol/first-touches` (BASIC)
199
+
200
+ The **globally earliest buy by any tracked KOL** per token the discovery signal. Each event carries the entry size in ETH, `tx_hash`, `token_age_minutes` at first touch, the MC at entry, and the current + peak MC so you can score how the call aged.
201
+
202
+ ```ts
203
+ const { events, next_before } = await client.kol.firstTouches({
204
+ limit: 50, // 1–100 — clamped to 20 below PRO
205
+ token_age_max_min: 60, // only tokens under an hour old at first touch
206
+ min_eth: 0.1,
207
+ // since: lastSeen, // poll forward
208
+ // before: next_before, // page back
209
+ });
210
+ ```
211
+
212
+ `first_kol.evm_address` is ULTRA-only; `name` and `twitter_url` are always returned.
213
+
214
+ ### `client.kol.wallet(wallet)` — `GET /rhc/kol/{wallet}` (BASIC)
215
+
216
+ Aggregate stats over one KOL's last 200 RHC trades plus their 50 most recent.
217
+
218
+ ```ts
219
+ const profile = await client.kol.wallet("0xabc…");
220
+ console.log(profile.kol_name, profile.stats.net_eth, "ETH net");
221
+ ```
222
+
223
+ ## DEX trade tape — `client.trades(params?)` — `GET /rhc/trades` (PRO+)
224
+
225
+ Every Uniswap v2/v3/v4 swap on chain 4663, ~sub-second from execution. Each row carries the effective trading account (`trader_eoa` — `tx.from` normally, or the ERC-4337 userOp sender when the trade was bundled; never the router or the bundler), gas/ordering for MEV work, pool state, and KOL/deployer flags. Cursor via `next_before`.
226
+
227
+ ```ts
228
+ const { trades } = await client.trades({
229
+ token: "0xdef…", // filter to one token
230
+ dex: "uniswap-v3", // "uniswap-v2" | "uniswap-v3" | "uniswap-v4"
231
+ min_eth: 1,
232
+ limit: 100,
233
+ });
234
+ for (const t of trades) {
235
+ console.log(t.trader_eoa, t.action, t.eth_amount, "ETH", "gas", t.gas_price, "gwei", t.is_kol ? `(KOL ${t.kol_name})` : "");
236
+ }
237
+ ```
238
+
239
+ ## Liquidity removals — `client.lpEvents(params?)` — `GET /rhc/lp-events` (PRO+)
240
+
241
+ The rug signal. Uniswap v2/v3 `Burn` and v4 `ModifyLiquidity` with a negative delta on tracked pools, straight from our own node's log subscription — each row enriched with the token, the wallet that pulled (`provider`), `provider_is_token_deployer` (the classic rug shape), deployer tier and KOL name. Cursor via `next_before` (same opaque keyset as `client.trades()`).
242
+
243
+ ```ts
244
+ const { events, has_more, next_before, coverage } = await client.lpEvents({ token: "0xdef…", limit: 50 });
245
+ for (const ev of events) {
246
+ if (ev.provider_is_token_deployer) console.warn("deployer pulled liquidity:", ev.tx_hash, ev.dex, ev.token_amount_raw);
247
+ }
248
+ console.log(coverage.adds_persisted); // false — always
249
+ ```
250
+
251
+ > **Removals ONLY.** Liquidity adds are not persisted (v4 adds share the topic and are dropped at decode; v2/v3 `Mint` is not subscribed), so every row is `event: "remove"` and an empty page means "no removals seen", never "no liquidity activity" — the `coverage` block spells this out. Amounts are **raw uint256 decimal strings** (`liquidity`, `amount0` / `amount1`, plus pre-resolved `token_amount_raw` / `quote_token` / `quote_amount_raw`) — do not `Number()` them; v4 rows carry `liquidity` only because the pool manager emits no token amounts. Filters: `token`, `pool` (v2/v3 address or v4 bytes32 poolId), `provider`, `dex`. Data since 2026-08-05.
252
+
253
+ ## Token intelligence`client.tokens`
254
+
255
+ | Method | Endpoint | Tier | Returns |
256
+ |---|---|---|---|
257
+ | `list(params?)` | `/rhc/tokens` | PRO+ | Live-priced token discovery — MC, liquidity, peak MC + drawdown, launchpad, deployer tier. Sort by `last_trade` / `market_cap` / `liquidity` / `peak_mc`. |
258
+ | `equities(params?)` | `/rhc/equities` | BASIC | Every official Robinhood tokenized stock/ETF **beacon-verified identity**, live price / MC / liquidity, 24h trades / ETH volume / buyers vs sellers. Sort by `volume` / `trades` / `market_cap` / `last_trade` / `symbol`; `symbol` (exact) / `q` (substring) filters. |
259
+ | `get(address)` | `/rhc/tokens/{address}` | BASIC | Full snapshot: metadata, price/MC/FDV, peak + drawdown, deployer reputation block, KOL activity, pool inventory. |
260
+ | `candles(address, params?)` | `/rhc/tokens/{address}/candles` | PRO+ | 1-minute price + market-cap OHLC, close liquidity, volume with buy/sell split, trade counts. |
261
+ | `kolConsensus(address)` | `/rhc/tokens/{address}/kol-consensus` | PRO+ | KOL buyers vs sellers, `kol_exit_rate`, `net_flow_eth`, median entry MC, first touch. ULTRA adds buyer/exited wallet lists. |
262
+ | `buyerQuality(address)` | `/rhc/tokens/{address}/buyer-quality` | BASIC | 0–100 first-20 buyer-cohort quality win-rate, KOL presence, bot-domination, bundle-buyer legs, dump-cluster ensemble. |
263
+ | `bundle(address)` | `/rhc/tokens/{address}/bundle` | BASIC | Same-block launch-bundle detection + how much of what the cohort bought it still holds. |
264
+ | `batch(addresses)` | `POST /rhc/token/batch` | BASIC | Up to **50** tokens in one call metadata, price/MC/FDV/liquidity, peak MC, deployer reputation. |
265
+ | `batchBuyerQuality(addresses)` | `POST /rhc/tokens/batch/buyer-quality` | BASIC | Up to **20** tokens' early-buyer quality scores in one call. |
266
+ | `topTraders(address, params?)` | `/rhc/tokens/{address}/top-traders` | PRO+ | Lifetime per-trader performance on one token, ranked by realized ETH, with win-rate / bot / KOL / dump-cluster enrichment. |
267
+ | `flow(address, window?)` | `/rhc/tokens/{address}/flow` | PRO+ | Net buy/sell split by trader cohort — who is accumulating and who is distributing. |
268
+ | `peakHistory(address, params?)` | `/rhc/tokens/{address}/peak-history` | PRO+ | Peak MC, drawdown, and a running high-water curve. Returns both the recorded and the candle-derived observed peak. |
269
+ | `risk(address)` | `/rhc/tokens/{address}/risk` | PRO+ | EVM-native risk computed **live**: proxy upgradeability, mint/pause capability, LP custody, and a live honeypot sell-simulation. |
270
+ | `holders(address, params?)` | `/rhc/tokens/{address}/holders` | PRO+ | Exact holder set + concentration, folded from ERC-20 `Transfer` logs and reconciled against on-chain `totalSupply()`, plus `holder_growth` (1h/24h/7d entered / exited / net). |
271
+
272
+ ### Tokenized stocks & ETFs — `equities(params?)` (BASIC)
273
+
274
+ ```ts
275
+ const { equities, total_equities, identity } = await client.tokens.equities({ sort: "volume", limit: 20 });
276
+ for (const e of equities) {
277
+ console.log(e.symbol, e.name, e.price_usd, "MC", e.market_cap_usd, "24h", e.trades_24h, "trades", e.volume_eth_24h, "ETH", `${e.buyers_24h}b/${e.sellers_24h}s`);
278
+ }
279
+ const nvda = await client.tokens.equities({ symbol: "NVDA" }); // exact ticker, case-insensitive
280
+ console.log(identity.method, identity.issuer_beacon); // "beacon", 0xe10b…151b00
281
+ ```
282
+
283
+ > **Identity is the issuer BEACON, never the name.** A token is listed only if its contract is an EIP-1967 beacon proxy on Robinhood's issuer beacon, read from our own node every 10 minutes — `verified` is `true` by construction and `issuer_beacon` says which beacon. Name-matching would have listed 20 fake "GameStop • Robinhood Token" contracts and 8 fake NVDAs on ship day; none appear here. `name` has the "• Robinhood Token" suffix stripped for display; `onchain_name` is the raw ERC-20 name. 24h stats are cached 60 s (`stats_as_of`).
284
+
285
+ ### Who is actually making money — `topTraders(address, params?)` (PRO+)
286
+
287
+ ```ts
288
+ const { traders } = await client.tokens.topTraders("0xdef…", { limit: 25 });
289
+ for (const t of traders) {
290
+ console.log(t.trader_eoa, t.net_eth, t.win_rate, t.likely_bot ? "(bot)" : "");
291
+ }
292
+ ```
293
+
294
+ > **`net_eth` is REALIZED flow (`sell − buy`), not PnL.** It does not value a trader's remaining bag, so a wallet that bought and still holds ranks **last**, not first. For FIFO cost-basis PnL use `client.wallet.pnl()`.
295
+
296
+ ### Who is buying vs dumping `flow(address, window?)` (PRO+)
297
+
298
+ ```ts
299
+ const { cohorts } = await client.tokens.flow("0xdef…", "24h");
300
+ // net_eth = sell − buy, so POSITIVE means that cohort DISTRIBUTED.
301
+ const bots = cohorts.find((c) => c.cohort === "bot");
302
+ const smart = cohorts.find((c) => c.cohort === "smart_money");
303
+ ```
304
+
305
+ Cohorts are mutually exclusive, assigned by priority: `kol` → `bot` → `dump_cluster` → `early_buyer` → `unprofiled` → `smart_money` → `retail`. `smart_money` is derived (win-rate ≥ 0.5 and net positive), and `unprofiled` is a real answer — that trader simply has not met the reputation thresholds yet.
306
+
307
+ ### How far off the top — `peakHistory(address, params?)` (PRO+)
308
+
309
+ ```ts
310
+ const p = await client.tokens.peakHistory("0xdef…", { window: "7d" });
311
+ console.log(p.peak.drawdown_from_peak, p.peak.peak_mc_usd_recorded, p.peak.peak_mc_usd_observed);
312
+ ```
313
+
314
+ > **Two peaks are returned because they disagree.** `peak_mc_usd_recorded` is the stored high-water mark that deployer runner-rate and the $40K graduation bar key off; it is sampled from write batches, so it can undercount an intra-batch spike. `peak_mc_usd_observed` is the max of 1-minute candle highs — trade-level truth, and always ≥ recorded. Candle history begins 2026-07-15, so check `observed_covers_full_history` before treating the observed figure as a lifetime maximum.
315
+
316
+ ### Can I actually sell this — `risk(address)` (PRO+)
317
+
318
+ ```ts
319
+ const r = await client.tokens.risk("0xdef…");
320
+ if (r.sellability.sellable === "no") return; // bought-but-cannot-sell
321
+ if (r.flags.includes("upgradeable") || r.capabilities.can_mint) { /* treat with care */ }
322
+ ```
323
+
324
+ > **This is not the Solana risk model.** EVM has no mint or freeze authority: across 300 random Robinhood Chain tokens only **2.3%** even expose an owner function and **0%** expose `mint` in their own bytecode — so an absent flag is the norm, **not** a safety signal. The signals that discriminate here are proxy upgradeability, LP custody and above all **sellability**, which is simulated at the chain head and never cached, because whether a token can be sold changes the instant an owner flips a setting. Note `owner.model: "none"` (no owner function at all) is a different answer from `"renounced"`.
325
+
326
+ ### Who holds it — `holders(address, params?)` (PRO+)
327
+
328
+ ```ts
329
+ const h = await client.tokens.holders("0xdef…", { limit: 50 });
330
+ if (!h.verified) console.warn("unverified:", h.unverified_reason);
331
+ console.log(h.concentration?.top10_share, h.concentration?.pool_held_pct);
332
+ const g = h.holder_growth?.["24h"];
333
+ if (g) console.log(`24h: +${g.entered_still_holding} / -${g.exited} → net ${g.net}`);
334
+ ```
335
+
336
+ > Balances are folded from ERC-20 `Transfer` logs — **not** derived from trades — and reconciled against on-chain `totalSupply()` at a pinned block. **Check `verified` first**: `false` means the reconstruction is incomplete for that token and `unverified_reason` says why. Concentration **excludes liquidity pools and burn addresses** from the circulating denominator (the largest holder of a token is otherwise its own pool) and reports them separately as `pool_held_pct` / `burned_pct`. `balance` is a raw uint256 returned as a decimal **string** — do not `Number()` it. Holder addresses may be ERC-4337 smart accounts, so `holder_count` is not a headcount of people.
337
+ >
338
+ > **`holder_growth`** (`RhcHolderGrowthWindow`, keys `"1h"` / `"24h"` / `"7d"` + `note`) reports per window: `entered` (addresses whose first `Transfer` of the token landed at-or-after `cutoff_block`, any current balance), `entered_still_holding` (those still non-zero), `exited` (pre-existing holders whose last `Transfer` in the window left them at zero) and `net` = `entered_still_holding − exited` ≈ Δ `holder_count`. Pools and burns are excluded. A window is `null` only when the chain had no ingested trades in it; the whole object is `null` only if the growth read failed. This is possible because balances are folded from `Transfer` logs with history retained — the Solana census cannot answer it.
339
+
340
+ ```ts
341
+ // Launch-bundle + quality gate before buying
342
+ const { bundle } = await client.tokens.bundle("0xdef…");
343
+ const quality = await client.tokens.buyerQuality("0xdef…");
344
+ if (bundle.bundle_kind === "same_block" && (bundle.held_pct_of_supply ?? 0) > 0.2 && !bundle.fully_exited) {
345
+ // bundle still sitting on supply — it can dump
346
+ }
347
+ if (quality.quality.signal === "negative") { /* skip */ }
348
+ ```
349
+
350
+ > **EVM note:** Robinhood Chain is an Arbitrum Orbit L2 with no atomic multi-signer transaction, so a detected bundle is `bundle_kind: "same_block"` (or `"none"`) — there is no `atomic_tx` kind. KOL consensus is denominated in ETH (`net_flow_eth`).
351
+
352
+ ### Batch reads
353
+
354
+ ```ts
355
+ // Up to 50 tokens, one round-trip. Set-based server-side, not a fan-out of get().
356
+ // Every requested address is echoed back — unknown ones as { found: false } — so
357
+ // positions line up with what you sent.
358
+ const { tokens, requested, found } = await client.tokens.batch([token1, token2, token3]);
359
+ for (const t of tokens) {
360
+ if (t.found) console.log(t.symbol, t.market_cap_usd, t.deployer?.tier);
361
+ }
362
+
363
+ // Early-buyer quality for several tokens. MAX 20 not the Solana batch cap of 50,
364
+ // because each token is a per-token cohort computation (early-buyer scan + bundle
365
+ // detection + alpha/cluster joins), so 50 would mean ~200 round-trips behind one
366
+ // request. The cap comes back as `max_addresses`. A token that fails to score
367
+ // degrades to an entry carrying `error` instead of failing the whole batch.
368
+ const { tokens: scored } = await client.tokens.batchBuyerQuality([token1, token2]);
369
+ for (const q of scored) {
370
+ if ("error" in q) console.warn(q.token_address, q.error);
371
+ else console.log(q.token_address, q.quality.score, q.quality.signal);
372
+ }
373
+ ```
374
+
375
+ ## Deployer reputation — `client.deployerHunter`
376
+
377
+ Most RHC launchpads are direct-to-DEX (no bonding curve), so "graduation" is a market-cap milestone: `graduation_rate` = share of a deployer's tokens that reached a **$40K+** peak MC; `runner_rate` = share that reached **$100K+**. `tier` is `elite` / `good` / `neutral` / `spammer`.
378
+
379
+ > **Tier semantics (migrations 267 + 269).** `elite` / `good` are earned on the **$100K `runner_rate`** *and* require **24h of deployer history** — the $40K bar proved farmable by operators mass-relaunching one ticker across rotating wallets, and a wallet minutes old can hit 5 launches on RHC. `graduation_rate` still means the $40K bar and is still returned everywhere, but it **no longer sets the tier**; `spammer` is the one label that still keys off it. Ranking by `graduation_rate` is ranking on a metric the tier ignores. `stats()` returns the thresholds actually in force.
380
+
381
+ | Method | Endpoint | Tier | Returns |
382
+ |---|---|---|---|
383
+ | `leaderboard(params?)` | `/rhc/deployer-hunter/leaderboard` | BASIC | 40k+ deployers ranked over a 5-min-refresh rollup. |
384
+ | `profile(address)` | `/rhc/deployer-hunter/{address}` | BASIC | Reputation row + 50 most recent tokens. |
385
+ | `trajectory(address)` | `/rhc/deployer-hunter/{address}/trajectory` | BASIC | Getting better or worse streaks, rolling 10-launch success curve, trend, cadence. |
386
+ | `tokens(address, params?)` | `/rhc/deployer-hunter/{address}/tokens` | BASIC | Full paginated launch history with live + peak MC and liquidity. |
387
+ | `history(address, params?)` | `/rhc/deployer-hunter/{address}/history` | PRO+ | Deploy history + reputation row, exact `total`, `graduated_pool`. |
388
+ | `bestTokens(params?)` | `/rhc/deployer-hunter/best-tokens` | BASIC | Highest-peaking tokens from reputable (elite/good) deployers in a window. |
389
+ | `stats()` | `/rhc/deployer-hunter/stats` | BASIC | Chain-wide summary — population per tier, spam share, alert volume, active `tier_rules`. |
390
+ | `alerts(params?)` | `/rhc/deployer-hunter/alerts` | BASIC | New-deploy / graduation signal feed, tradability-filtered, read-time tier. |
391
+ | `recentBonds(params?)` | `/rhc/deployer-hunter/recent-bonds` | BASIC | Recent $40K graduations, newest peak first. |
392
+
393
+ ```ts
394
+ // Leaderboard — 40k+ deployers, 5-min-refresh rollup
395
+ const { deployers, has_more } = await client.deployerHunter.leaderboard({
396
+ sort: "runner_rate", // graduation_rate | runner_rate | tokens_deployed | best_peak_mc_usd | last_deploy_at
397
+ tier: "elite",
398
+ min_tokens: 3,
399
+ limit: 20,
400
+ offset: 0,
401
+ });
402
+
403
+ // One deployer — unknown wallets return 200 with is_deployer:false (not a 404)
404
+ const { is_deployer, deployer, recent_tokens } = await client.deployerHunter.profile("0xabc…");
405
+ ```
406
+
407
+ ### Is this deployer improving? — `trajectory(address)` (BASIC)
408
+
409
+ Current and longest hit/miss streaks, a rolling 10-launch success rate, best/worst stretches, average days between deploys, and how many launches they burn between a miss and the next hit.
410
+
411
+ ```ts
412
+ const { trajectory, success_metric, truncated } = await client.deployerHunter.trajectory("0xabc…");
413
+ console.log(trajectory?.trend, trajectory?.current_streak, success_metric);
414
+ ```
415
+
416
+ The per-token success event here is the **$40K graduation** (echoed as `success_metric`), deliberately *not* the $100K runner bar that sets tiers — $100K is rare enough that most deployers would return an all-zero curve, and a trajectory needs events to have a shape. Analysis is capped at 500 launches; `truncated` tells you whether the curve is the whole story.
417
+
418
+ ### Launch history `tokens(address, params?)` (BASIC) and `history(address, params?)` (PRO+)
419
+
420
+ ```ts
421
+ // Enumerable launch history with live MC, peak MC and liquidity
422
+ const { tokens, total, has_more, sort_scope } = await client.deployerHunter.tokens("0xabc…", {
423
+ limit: 50, // 1–100
424
+ offset: 0, // 0–10000
425
+ sort: "first_seen_at", // "first_seen_at" | "peak_mc_usd"
426
+ });
427
+
428
+ // PRO+ the same history with graduated_pool and an exact total
429
+ const hist = await client.deployerHunter.history("0xabc…", { limit: 100, offset: 0 });
430
+ ```
431
+
432
+ `sort: "peak_mc_usd"` orders the fetched **page** only (the response echoes `sort_scope: "page"`), because peak MC lives in another table — it is not a global top-tokens ranking. Use `bestTokens()` for that. `profile()` caps `recent_tokens` at 50 and is a point-in-time read; `tokens()` is the enumerable list.
433
+
434
+ ### Best tokens + chain stats
435
+
436
+ ```ts
437
+ // What did the deployers worth tracking actually produce?
438
+ const { tokens, reputable_deployers, truncated } = await client.deployerHunter.bestTokens({
439
+ period: "7d", // "24h" | "7d" | "30d" | "all"
440
+ limit: 10, // 1–50
441
+ });
442
+
443
+ // The denominator for "is this deployer rare?"
444
+ const stats = await client.deployerHunter.stats();
445
+ console.log(stats.by_tier, stats.spam_token_share, stats.tier_rules.elite, stats.runner_definition);
446
+ ```
447
+
448
+ `bestTokens()` is gated on reputation rather than raw peak MC the unfiltered version is `client.tokens.list({ sort: "peak_mc" })`. When `truncated` is true the top-N was drawn from the 1000 most *recent* launches in the period rather than the whole period.
449
+
450
+ ### Deployer alerts — `alerts(params?)` (BASIC)
451
+
452
+ New deploys and graduations from tracked deployers, newest first. Poll forward with `since: next_event_at`, page back with `before: next_before`. ULTRA gets the full limit; BASIC/PRO share a 50-alert cap.
453
+
454
+ ```ts
455
+ const { alerts, tradability_filter, next_event_at } = await client.deployerHunter.alerts({
456
+ deployer_tier: "elite", // filters on the RESOLVED tier
457
+ alert_type: "new_deploy", // "new_deploy" | "graduated"
458
+ priority: "high", // "high" | "medium"
459
+ min_mc: 10_000,
460
+ limit: 50,
461
+ // include_untradeable: true, // opt out of the liquidity gate
462
+ });
463
+ for (const a of alerts) {
464
+ console.log(a.token_symbol, a.tier, a.liquidity_usd, a.tier_is_stale ? `(was ${a.tier_at_alert})` : "");
465
+ }
466
+ ```
467
+
468
+ Two things worth knowing:
469
+
470
+ - **Tradability is filtered by default.** Alerts on tokens with `liquidity_usd` below **$100** are dropped — unknown liquidity included, since on RHC that usually means a drained pool — because a $45K-MC alert on a $68 pool is not a signal. Pass `include_untradeable: true` for the raw tape; the active setting comes back as `tradability_filter`.
471
+ - **`tier` is resolved at read time** from the live reputation view, so an alert can never advertise a reputation the deployer has since lost. The snapshot written when the alert fired is returned as `tier_at_alert`, with `tier_is_stale` flagging drift, and `deployer_tier=` filters on the resolved value so the filter and the payload always agree.
472
+
473
+ ### Recent graduations — `recentBonds(params?)` (BASIC)
474
+
475
+ ```ts
476
+ const { tokens, graduation_mc } = await client.deployerHunter.recentBonds({
477
+ deployer_tier: "good",
478
+ min_peak: 100_000, // only raises the $40K floor, never lowers it
479
+ limit: 50, // 1–200
480
+ });
481
+ ```
482
+
483
+ On RHC a graduation is the **$40K peak-MC milestone**, not a bonding-curve completion noxa/pons/clanker launch direct-to-DEX with no curve so the set is defined purely by peak MC.
484
+
485
+ ## Smart-money wallets — `client.alphaWallets(params?)` — `GET /rhc/alpha-wallets` (PRO+)
486
+
487
+ The reverse of KOL discovery: rank Robinhood Chain trader wallets by realized on-chain performance. `net_eth` is realized net flow (sell − buy), `win_rate` is the share of tokens taken out profitably, `likely_bot` flags atomic-arb/MM fleets. RHC is dual-natured (launchpad memecoins vs tokenized stocks/stables), so filter with `min_memecoin_share` to isolate memecoin traders.
488
+
489
+ ```ts
490
+ const { wallets } = await client.alphaWallets({
491
+ classification: "smart_money", // all | human | bot | smart_money
492
+ identity: "unknown", // all | known_kol | unknown (net-new RHC smart money)
493
+ min_memecoin_share: 0.7,
494
+ sort: "net_eth", // net_eth | win_rate | trades | tokens | buy_eth | memecoin_share | last_trade_at
495
+ limit: 25,
496
+ });
497
+ ```
498
+
499
+ ## Rule engines push, not polling
500
+
501
+ Four server-side rule engines watch the Robinhood Chain tape for you and deliver over **webhook**, **WebSocket**, or both. **Every quota is per chain** — configuring RHC rules never consumes your Solana budget, and a full set of Solana rules leaves your RHC capacity untouched. A `webhook_secret` is returned **exactly once** on create (null when `delivery_mode` is `"websocket"`); payloads are signed HMAC-SHA256 over `` `<timestamp>.<body>` `` in the `X-MadeOnSol-Signature` header.
502
+
503
+ ### Copy-trade — `client.copyTrade` (PRO+)
504
+
505
+ ```ts
506
+ const { subscription, webhook_secret } = await client.copyTrade.create({
507
+ name: "degen desk",
508
+ source_wallets: ["0xaaa…", "0xbbb…", "0xccc…"], // 1–250, per-tier cap enforced server-side
509
+ min_trade_eth: 0.01,
510
+ only_action: "buy", // buy | sell | both
511
+ sizing_mode: "fixed", // fixed | proportional | percent_source
512
+ sizing_amount: 0.05, // ETH when sizing_mode is "fixed"
513
+ delivery_mode: "websocket",
514
+ });
515
+
516
+ await client.copyTrade.update(subscription.id, { is_active: false });
517
+ await client.copyTrade.delete(subscription.id); // fired signals cascade
518
+
519
+ // Catch-up path for a missed webhook / dropped WS — fires retained 7 days
520
+ const since = new Date(Date.now() - 3_600_000).toISOString();
521
+ const { signals } = await client.copyTrade.signals({ subscription_id: subscription.id, since });
522
+ ```
523
+
524
+ Sizes are **ETH, not SOL**, and there is deliberately **no market-cap band** — the RHC trade event carries no market cap, so a band could only be a per-event DB lookup in the hot path of a ~3.3M-trades/day chain. `update()` re-checks the per-tier wallet cap, so a rule cannot be PATCHed past its limit.
525
+
526
+ ### Price alerts `client.priceAlerts` (PRO+)
527
+
528
+ ```ts
529
+ const { alert, evaluation } = await client.priceAlerts.create({
530
+ token_address: "0xdef…", // must already be tracked on RHC with a market cap
531
+ drop_pct: 30, // 0.01–99.99, measured from the MC captured RIGHT NOW
532
+ recovery_pct: 15, // omit for a dip-only, terminal alert
533
+ webhook_url: "https://example.com/hook",
534
+ });
535
+ console.log(evaluation.mode, evaluation.interval_seconds); // "polled", ~15
536
+
537
+ const { events } = await client.priceAlerts.events({ alert_id: alert.id, event_type: "dip" });
538
+ ```
539
+
540
+ > **RHC price alerts are polled (~15s), not sub-second like the Solana ones.** `rhc_token_prices` is written by the RHC ingester on a separate box and emits no `pg_notify`, so there is nothing to react to — effective latency is that interval plus the token's own price-update cadence. Every create response spells this out in its `evaluation` block. The baseline MC is captured at creation, so an alert is a delta from the moment you set it; alerts self-expire after 30 days, and only `name`, `delivery_mode`, `webhook_url` and `is_active` are mutable (retuning a threshold mid-flight would make the recorded events uninterpretable).
541
+
542
+ ### KOL coordination rules `client.kol.coordinationAlerts` (PRO+)
543
+
544
+ ```ts
545
+ const { rule, scoring } = await client.kol.coordinationAlerts.create({
546
+ min_kols: 3, // 2–50 distinct tracked KOL buyers
547
+ window_minutes: 15, // 1–60 rolling window
548
+ min_score: 40, // 0–100
549
+ cooldown_min: 30, // 1–1440 before the same token can fire again
550
+ score_jump_break: 20, // score jump that breaks the cooldown early
551
+ delivery_mode: "websocket",
552
+ });
553
+ await client.kol.coordinationAlerts.update(rule.id, { min_kols: 4 }); // UUID id
554
+ ```
555
+
556
+ > **Coordination scoring is comparable to Solana, but not identical.** The shared v1 scorer runs, `quality` is a real KOL win-rate, and `earliness` is **defaulted** — RHC has no early-entry equivalent. The create response's `scoring` block records which components are real, and every fired signal repeats it in `score_inputs`.
557
+
558
+ ### KOL first-touch subscriptions — `client.kol.firstTouchSubscriptions` (ULTRA+)
559
+
560
+ ```ts
561
+ const { subscription } = await client.kol.firstTouchSubscriptions.create({
562
+ name: "early hands",
563
+ filters: {
564
+ min_first_buy_eth: 0.05,
565
+ min_kol_winrate: 0.5, // win-rate on CLOSED positions
566
+ strategy: "swing", // scalper | day_trader | swing | inactive | unscored
567
+ min_mc_usd: 10_000,
568
+ },
569
+ delivery_mode: "websocket",
570
+ });
571
+
572
+ // `filters` is a whole-object REPLACE, not a merge {} clears every filter
573
+ await client.kol.firstTouchSubscriptions.update(subscription.id, { filters: {} });
574
+ ```
575
+
576
+ > **First-touch filters are not the Solana set.** RHC has no scout score, so `min_scout_tier` and `min_n_touches` do not exist here rather than silently matching nothing; `min_kol_winrate` and `strategy` are the quality gates. Unknown filter keys are rejected with a **400**, not ignored.
577
+
578
+ ## Streaming `client.stream` (PRO+)
579
+
580
+ Managed WebSocket with token fetch on every (re)connect, auto-reconnect with backoff, heartbeat liveness, and typed events. Stream tokens **never expire** (since 2026-08-27) — there is no refresh timer; `client.stream.getToken()` returns the same token every call (`expires_at` / `next_refresh_at` are always `null`), and `getToken({ rotate: true })` replaces it (the old one keeps working for 60 s). Six RHC channels:
581
+
582
+ | Channel | Emits | Tier | Scope |
583
+ |---|---|---|---|
584
+ | `rhc:kol_trades` | `rhc:kol_trade` | PRO+ | broadcastthe live KOL tape |
585
+ | `rhc:dex_trades` | `rhc:dex_trade` | **ULTRA+** | broadcast — the full DEX firehose |
586
+ | `rhc:copytrade:signals` | `rhc:copytrade:signal` | PRO+ | user-scoped — only **your** rules' fires |
587
+ | `rhc:price_alert:events` | `rhc:price_alert:dip`, `rhc:price_alert:recovery` | PRO+ | user-scoped; ~15s polled, not sub-second |
588
+ | `rhc:kol:coordination` | `rhc:kol:coordination` | PRO+ | user-scoped — only **your** rules' fires |
589
+ | `rhc:kol:first_touches` | `rhc:kol:first_touch` | PRO+ | broadcast — ULTRA gates only the first-touch *subscription CRUD*, not this channel |
590
+
591
+ > **Deprecated:** `rhc:trades` was never a real channel — 0.4.0 subscribers got a `channels_rejected` warning and silence. The server now accepts it as an alias of `rhc:dex_trades` (and acks it under the canonical name), and the SDK keeps the literal marked `@deprecated` so 0.4.0 code compiles. Use `rhc:dex_trades`.
592
+
593
+ ```ts
594
+ const stream = client.stream.connect();
595
+
596
+ stream
597
+ .on("open", () => console.log("connected"))
598
+ .on("rhc:kol_trade", (trade) => console.log("KOL trade", trade))
599
+ .on("rhc:dex_trade", (trade) => console.log("DEX trade", trade))
600
+ // New in 0.5.0 — the server tells you when a channel was refused (typo or
601
+ // tier gate); 0.4.0 dropped this frame and the stream just stayed silent.
602
+ .on("warning", (w) => console.warn("rejected:", w.code, w.rejected, w.valid_channels))
603
+ .on("error", (err) => console.error(err));
604
+
605
+ stream.subscribe(["rhc:kol_trades", "rhc:dex_trades"]);
606
+ // …later
607
+ stream.close(); // clean shutdown — short-lived scripts exit promptly
608
+ ```
609
+
610
+ On **Node < 22**, install the optional `ws` package (`npm i ws`) for the fastest clean exit; on Node 22 and in browsers the platform WebSocket is used automatically. You can also inject an implementation via `client.stream.connect({ WebSocketImpl })`.
611
+
612
+ ## Error handling
613
+
614
+ Every method throws `RobinhoodError` on a non-2xx response, with `.status`, `.body`, `.message`, and `.requestId` (the API's `_rid` — include it when reporting issues). GET requests retry network failures, rate-limits (`429`) and transient server errors (`5xx`) up to `maxRetries`, with exponential backoff and `Retry-After` / `X-RateLimit-Reset` hints.
615
+
616
+ POST, PATCH and DELETE requests have **no automatic retries**, regardless of `maxRetries`. This includes rule creation, updates/deletion, wallet tracking, stream-token retrieval/rotation, and the two POST-based batch reads. A network failure or server error may arrive **after the server applied a change**. Inspect the current rule, watchlist or token state before deciding whether to issue another mutation; do not wrap creates or rotations in a blind retry loop. This client policy prevents automatic replay, but does not provide server-side idempotency or exactly-once execution.
617
+
618
+ ```ts
619
+ import { RobinhoodError } from "robinhood-chain-sdk";
620
+
621
+ try {
622
+ await client.trades({ limit: 100 }); // PRO+
623
+ } catch (err) {
624
+ if (err instanceof RobinhoodError) {
625
+ if (err.status === 403) console.error("Upgrade required:", err.message);
626
+ else console.error(err.status, err.requestId, err.message);
627
+ }
628
+ }
629
+ ```
630
+
631
+ ## Types & constants
632
+
633
+ Fully-typed responses and params for all 54 endpoints are exported (`RhcKolFeedResponse`, `RhcKolCoordinationResponse`, `RhcKolFirstTouchesResponse`, `RhcTradesResponse`, `RhcTokenSnapshot`, `RhcTokenBatchResponse`, `RhcBatchBuyerQualityResponse`, `RhcBundleResponse`, `RhcTopTradersResponse`, `RhcFlowResponse`, `RhcPeakHistoryResponse`, `RhcRiskResponse`, `RhcHoldersResponse`, `RhcHolderGrowthWindow`, `RhcEquitiesResponse`, `RhcEquity`, `RhcLpEventsResponse`, `RhcLpEvent`, `RhcDeployerTrajectoryResponse`, `RhcDeployerTokensResponse`, `RhcDeployerHistoryResponse`, `RhcBestTokensResponse`, `RhcDeployerStatsResponse`, `RhcDeployerAlertsResponse`, `RhcRecentBondsResponse`, `RhcAlphaWalletsResponse`, plus the rule engines: `RhcCopyTradeSubscription`, `RhcCopyTradeCreateParams`, `RhcCopyTradeSignal`, `RhcPriceAlert`, `RhcPriceAlertEvaluation`, `RhcPriceAlertEvent`, `RhcCoordinationAlertRule`, `RhcCoordinationAlertScoring`, `RhcFirstTouchSubscription`, `RhcFirstTouchFilters`, `RhcDeletedResponse`, …), plus shared types (`DeployerTier`, `TradeAction`, `UniswapVersion`, `DeliveryMode`, `RhcBundleKind`, `RhcAlertType`, `RhcAlertPriority`, `RhcCoordinationSignal`) and the `CHAIN_ID` constant (`4663`).
634
+
635
+ ## Links
636
+
637
+ - **Robinhood Chain** — [madeonsol.com/robinhood](https://madeonsol.com/robinhood)
638
+ - **Pricing & free API key** — [madeonsol.com/pricing](https://madeonsol.com/pricing) (Robinhood Chain bundled into every tier)
639
+ - **API docs** — [madeonsol.com/api-docs](https://madeonsol.com/api-docs)
640
+ - **npm** — [robinhood-chain-sdk](https://www.npmjs.com/package/robinhood-chain-sdk)
641
+ - **GitHub** — [madeonsol/robinhood-chain-sdk](https://github.com/madeonsol/robinhood-chain-sdk)
642
+
643
+ ## License
644
+
645
+ MIT © MadeOnSol