robinhood-chain-sdk 0.8.1 → 0.9.0
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 +675 -641
- package/dist/index.d.ts +26 -14
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +14 -11
- package/dist/index.js.map +1 -1
- package/dist/stream.d.ts +300 -22
- package/dist/stream.d.ts.map +1 -1
- package/dist/stream.js +674 -36
- package/dist/stream.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/llms.txt +112 -112
- package/package.json +74 -74
package/README.md
CHANGED
|
@@ -1,641 +1,675 @@
|
|
|
1
|
-
# robinhood-chain-sdk
|
|
2
|
-
|
|
3
|
-
[](https://www.npmjs.com/package/robinhood-chain-sdk)
|
|
4
|
-
[](https://www.npmjs.com/package/robinhood-chain-sdk)
|
|
5
|
-
[](https://github.com/madeonsol/robinhood-chain-sdk)
|
|
6
|
-
[](https://www.typescriptlang.org/)
|
|
7
|
-
[](package.json)
|
|
8
|
-
[](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.
|
|
19
|
-
|
|
20
|
-
> **New in 0.8.
|
|
21
|
-
|
|
22
|
-
> **New in 0.
|
|
23
|
-
|
|
24
|
-
> **New in 0.
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
```
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
|
72
|
-
|
|
73
|
-
|
|
|
74
|
-
|
|
|
75
|
-
|
|
|
76
|
-
|
|
|
77
|
-
|
|
|
78
|
-
|
|
|
79
|
-
|
|
|
80
|
-
|
|
|
81
|
-
|
|
|
82
|
-
|
|
|
83
|
-
|
|
|
84
|
-
|
|
|
85
|
-
|
|
|
86
|
-
|
|
|
87
|
-
|
|
|
88
|
-
|
|
|
89
|
-
|
|
|
90
|
-
|
|
|
91
|
-
|
|
|
92
|
-
|
|
|
93
|
-
|
|
|
94
|
-
|
|
|
95
|
-
|
|
|
96
|
-
|
|
|
97
|
-
|
|
|
98
|
-
|
|
|
99
|
-
|
|
|
100
|
-
|
|
|
101
|
-
|
|
|
102
|
-
|
|
|
103
|
-
|
|
|
104
|
-
|
|
|
105
|
-
|
|
|
106
|
-
|
|
|
107
|
-
|
|
|
108
|
-
|
|
|
109
|
-
|
|
|
110
|
-
|
|
|
111
|
-
|
|
|
112
|
-
|
|
|
113
|
-
|
|
|
114
|
-
|
|
|
115
|
-
|
|
|
116
|
-
|
|
|
117
|
-
|
|
|
118
|
-
|
|
|
119
|
-
|
|
|
120
|
-
|
|
|
121
|
-
|
|
|
122
|
-
|
|
|
123
|
-
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
- **
|
|
132
|
-
- **
|
|
133
|
-
- **
|
|
134
|
-
- **
|
|
135
|
-
- **
|
|
136
|
-
- **
|
|
137
|
-
- **
|
|
138
|
-
- **
|
|
139
|
-
- **
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
|
258
|
-
|
|
259
|
-
| `
|
|
260
|
-
| `
|
|
261
|
-
| `
|
|
262
|
-
| `
|
|
263
|
-
| `
|
|
264
|
-
| `
|
|
265
|
-
| `
|
|
266
|
-
| `
|
|
267
|
-
| `
|
|
268
|
-
| `
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
const
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
```
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
//
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
|
384
|
-
|
|
385
|
-
| `
|
|
386
|
-
| `
|
|
387
|
-
| `
|
|
388
|
-
| `
|
|
389
|
-
| `
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
//
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
//
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
}
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
|
585
|
-
|
|
586
|
-
| `rhc:
|
|
587
|
-
| `rhc:
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
}
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
1
|
+
# robinhood-chain-sdk
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/robinhood-chain-sdk)
|
|
4
|
+
[](https://www.npmjs.com/package/robinhood-chain-sdk)
|
|
5
|
+
[](https://github.com/madeonsol/robinhood-chain-sdk)
|
|
6
|
+
[](https://www.typescriptlang.org/)
|
|
7
|
+
[](package.json)
|
|
8
|
+
[](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.9.0 — stream recovery: resume cursor, de-duplication, honest gaps.** The managed stream now tracks the cursor `{ instance, seq, ts }` of the last frame your handlers finished and resumes after it on every reconnect (the v1 `resume` request, with an automatic fallback to `replay_since_seq` / `replay_since_ts` on older servers). Delivery is at-least-once, de-duplicated by event `id`; new lifecycle events `cursor`, `replay`, `gap` (what could not be recovered — a `seq` gap is never loss) and `fatal`. Close codes are handled: 4001 re-fetches the token (bounded), 4002 waits ≥ 60 s instead of looping every second, 4003 stops, 4008 resumes; the backoff resets only after a `subscribed` ack. Every server `warning` frame is emitted (incl. `channels_rejected` / `channels_revoked`). `StreamChannel` / `STREAM_CHANNELS` now list all nine RHC channels (adds `rhc:dex_trades_unattributed`, `rhc:new_tokens`, `rhc:token_locks`); `RhcPriceAlertEvaluation.mode` is `"event_driven" | "polled"` with the new optional `trigger` / `fallback_poll_seconds`. See the stream section's "Recovery" notes.
|
|
19
|
+
|
|
20
|
+
> **New in 0.8.2 — mutation 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 automatically — which 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.
|
|
21
|
+
>
|
|
22
|
+
> **New in 0.8.1 — stream 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.
|
|
23
|
+
|
|
24
|
+
> **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.
|
|
25
|
+
|
|
26
|
+
> **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.
|
|
27
|
+
|
|
28
|
+
> **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.
|
|
29
|
+
|
|
30
|
+
## New in 0.5.0 — stream fixes
|
|
31
|
+
|
|
32
|
+
No REST changes; everything below is about `client.stream`.
|
|
33
|
+
|
|
34
|
+
- **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.
|
|
35
|
+
- **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`.
|
|
36
|
+
- **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`).
|
|
37
|
+
|
|
38
|
+
## Quick start (10 seconds)
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
npm install robinhood-chain-sdk
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
```ts
|
|
45
|
+
import { RobinhoodClient } from "robinhood-chain-sdk";
|
|
46
|
+
|
|
47
|
+
const client = new RobinhoodClient({ apiKey: "msk_..." }); // free key at madeonsol.com/pricing
|
|
48
|
+
|
|
49
|
+
// Tokens being bought by 2+ tracked KOLs on Robinhood Chain right now
|
|
50
|
+
const { tokens } = await client.kol.hotTokens({ window: "1h" });
|
|
51
|
+
console.log(tokens[0]?.token_symbol, tokens[0]?.kols_buying, "KOLs, net", tokens[0]?.net_eth, "ETH");
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Requires **Node.js ≥ 18** (native `fetch`). Works in Cloudflare Workers, Vercel Edge, Bun, and Deno.
|
|
55
|
+
|
|
56
|
+
## Authentication
|
|
57
|
+
|
|
58
|
+
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.
|
|
59
|
+
|
|
60
|
+
```ts
|
|
61
|
+
const client = new RobinhoodClient({
|
|
62
|
+
apiKey: process.env.MADEONSOL_API_KEY!,
|
|
63
|
+
maxRetries: 2, // optional — GET retries on network errors / 429 / 5xx (default 2)
|
|
64
|
+
});
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## Every endpoint → SDK method
|
|
68
|
+
|
|
69
|
+
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.
|
|
70
|
+
|
|
71
|
+
| # | Endpoint | SDK method | Tier |
|
|
72
|
+
|---|---|---|---|
|
|
73
|
+
| 1 | `GET /rhc/kol/feed` | `client.kol.feed(params?)` | BASIC |
|
|
74
|
+
| 2 | `GET /rhc/kol/leaderboard` | `client.kol.leaderboard(params?)` | BASIC |
|
|
75
|
+
| 3 | `GET /rhc/kol/hot-tokens` | `client.kol.hotTokens(params?)` | BASIC |
|
|
76
|
+
| 4 | `GET /rhc/kol/coordination` | `client.kol.coordination(params?)` | BASIC |
|
|
77
|
+
| 5 | `GET /rhc/kol/first-touches` | `client.kol.firstTouches(params?)` | BASIC |
|
|
78
|
+
| 6 | `GET /rhc/kol/{wallet}` | `client.kol.wallet(wallet)` | BASIC |
|
|
79
|
+
| 7 | `GET /rhc/trades` | `client.trades(params?)` | PRO+ |
|
|
80
|
+
| 8 | `GET /rhc/tokens` | `client.tokens.list(params?)` | PRO+ |
|
|
81
|
+
| 9 | `GET /rhc/tokens/{address}` | `client.tokens.get(address)` | BASIC |
|
|
82
|
+
| 10 | `GET /rhc/tokens/{address}/candles` | `client.tokens.candles(address, params?)` | PRO+ |
|
|
83
|
+
| 11 | `GET /rhc/tokens/{address}/kol-consensus` | `client.tokens.kolConsensus(address)` | PRO+ |
|
|
84
|
+
| 12 | `GET /rhc/tokens/{address}/buyer-quality` | `client.tokens.buyerQuality(address)` | BASIC |
|
|
85
|
+
| 13 | `GET /rhc/tokens/{address}/bundle` | `client.tokens.bundle(address)` | BASIC |
|
|
86
|
+
| 14 | `GET /rhc/tokens/{address}/top-traders` | `client.tokens.topTraders(address, params?)` | PRO+ |
|
|
87
|
+
| 15 | `GET /rhc/tokens/{address}/flow` | `client.tokens.flow(address, window?)` | PRO+ |
|
|
88
|
+
| 16 | `GET /rhc/tokens/{address}/peak-history` | `client.tokens.peakHistory(address, params?)` | PRO+ |
|
|
89
|
+
| 17 | `GET /rhc/tokens/{address}/risk` | `client.tokens.risk(address)` | PRO+ |
|
|
90
|
+
| 18 | `GET /rhc/tokens/{address}/holders` | `client.tokens.holders(address, params?)` | PRO+ |
|
|
91
|
+
| 19 | `POST /rhc/token/batch` | `client.tokens.batch(addresses)` — max 50 | BASIC |
|
|
92
|
+
| 20 | `POST /rhc/tokens/batch/buyer-quality` | `client.tokens.batchBuyerQuality(addresses)` — **max 20** | BASIC |
|
|
93
|
+
| 21 | `GET /rhc/equities` | `client.tokens.equities(params?)` | BASIC |
|
|
94
|
+
| 22 | `GET /rhc/lp-events` | `client.lpEvents(params?)` | PRO+ |
|
|
95
|
+
| 23 | `GET /rhc/deployer-hunter/leaderboard` | `client.deployerHunter.leaderboard(params?)` | BASIC |
|
|
96
|
+
| 24 | `GET /rhc/deployer-hunter/best-tokens` | `client.deployerHunter.bestTokens(params?)` | BASIC |
|
|
97
|
+
| 25 | `GET /rhc/deployer-hunter/stats` | `client.deployerHunter.stats()` | BASIC |
|
|
98
|
+
| 26 | `GET /rhc/deployer-hunter/alerts` | `client.deployerHunter.alerts(params?)` | BASIC |
|
|
99
|
+
| 27 | `GET /rhc/deployer-hunter/recent-bonds` | `client.deployerHunter.recentBonds(params?)` | BASIC |
|
|
100
|
+
| 28 | `GET /rhc/deployer-hunter/{address}` | `client.deployerHunter.profile(address)` | BASIC |
|
|
101
|
+
| 29 | `GET /rhc/deployer-hunter/{address}/trajectory` | `client.deployerHunter.trajectory(address)` | BASIC |
|
|
102
|
+
| 30 | `GET /rhc/deployer-hunter/{address}/tokens` | `client.deployerHunter.tokens(address, params?)` | BASIC |
|
|
103
|
+
| 31 | `GET /rhc/deployer-hunter/{address}/history` | `client.deployerHunter.history(address, params?)` | PRO+ |
|
|
104
|
+
| 32 | `GET /rhc/alpha-wallets` | `client.alphaWallets(params?)` | PRO+ |
|
|
105
|
+
| 33 | `GET /rhc/copytrade/subscriptions` | `client.copyTrade.list()` | PRO+ |
|
|
106
|
+
| 34 | `POST /rhc/copytrade/subscriptions` | `client.copyTrade.create(params)` | PRO+ |
|
|
107
|
+
| 35 | `GET /rhc/copytrade/subscriptions/{id}` | `client.copyTrade.get(id)` | PRO+ |
|
|
108
|
+
| 36 | `PATCH /rhc/copytrade/subscriptions/{id}` | `client.copyTrade.update(id, params)` | PRO+ |
|
|
109
|
+
| 37 | `DELETE /rhc/copytrade/subscriptions/{id}` | `client.copyTrade.delete(id)` | PRO+ |
|
|
110
|
+
| 38 | `GET /rhc/copytrade/signals` | `client.copyTrade.signals(params?)` | PRO+ |
|
|
111
|
+
| 39 | `GET /rhc/price-alerts` | `client.priceAlerts.list()` | PRO+ |
|
|
112
|
+
| 40 | `POST /rhc/price-alerts` | `client.priceAlerts.create(params)` | PRO+ |
|
|
113
|
+
| 41 | `GET /rhc/price-alerts/{id}` | `client.priceAlerts.get(id)` | PRO+ |
|
|
114
|
+
| 42 | `PATCH /rhc/price-alerts/{id}` | `client.priceAlerts.update(id, params)` | PRO+ |
|
|
115
|
+
| 43 | `DELETE /rhc/price-alerts/{id}` | `client.priceAlerts.delete(id)` | PRO+ |
|
|
116
|
+
| 44 | `GET /rhc/price-alerts/events` | `client.priceAlerts.events(params?)` | PRO+ |
|
|
117
|
+
| 45 | `GET /rhc/kol/coordination/alerts` | `client.kol.coordinationAlerts.list()` | PRO+ |
|
|
118
|
+
| 46 | `POST /rhc/kol/coordination/alerts` | `client.kol.coordinationAlerts.create(params)` | PRO+ |
|
|
119
|
+
| 47 | `GET /rhc/kol/coordination/alerts/{id}` | `client.kol.coordinationAlerts.get(id)` | PRO+ |
|
|
120
|
+
| 48 | `PATCH /rhc/kol/coordination/alerts/{id}` | `client.kol.coordinationAlerts.update(id, params)` | PRO+ |
|
|
121
|
+
| 49 | `DELETE /rhc/kol/coordination/alerts/{id}` | `client.kol.coordinationAlerts.delete(id)` | PRO+ |
|
|
122
|
+
| 50 | `GET /rhc/kol/first-touches/subscriptions` | `client.kol.firstTouchSubscriptions.list()` | ULTRA+ |
|
|
123
|
+
| 51 | `POST /rhc/kol/first-touches/subscriptions` | `client.kol.firstTouchSubscriptions.create(params)` | ULTRA+ |
|
|
124
|
+
| 52 | `GET /rhc/kol/first-touches/subscriptions/{id}` | `client.kol.firstTouchSubscriptions.get(id)` | ULTRA+ |
|
|
125
|
+
| 53 | `PATCH /rhc/kol/first-touches/subscriptions/{id}` | `client.kol.firstTouchSubscriptions.update(id, params)` | ULTRA+ |
|
|
126
|
+
| 54 | `DELETE /rhc/kol/first-touches/subscriptions/{id}` | `client.kol.firstTouchSubscriptions.delete(id)` | ULTRA+ |
|
|
127
|
+
| + | `POST /stream/token` → WebSocket | `client.stream.connect()` | PRO+ |
|
|
128
|
+
|
|
129
|
+
## What you can build
|
|
130
|
+
|
|
131
|
+
- **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.
|
|
132
|
+
- **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.
|
|
133
|
+
- **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).
|
|
134
|
+
- **Discovery bot** — `client.kol.firstTouches()` gives the globally earliest KOL buy per token, filterable to tokens minutes old.
|
|
135
|
+
- **Rug watch** — `client.lpEvents({ token })` is the liquidity-removals feed (removals only, PRO+); flag `provider_is_token_deployer` rows the moment they land.
|
|
136
|
+
- **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.
|
|
137
|
+
- **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.
|
|
138
|
+
- **Portfolio / watchlist refresh** — `client.tokens.batch()` prices up to 50 tokens in one call, `client.tokens.batchBuyerQuality()` scores up to 20.
|
|
139
|
+
- **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`.
|
|
140
|
+
- **Deployer due-diligence** — `client.deployerHunter.leaderboard()` / `.profile()` / `.trajectory()` / `.tokens()` rank and profile 40k+ RHC deployers; `.stats()` gives the chain-wide denominator.
|
|
141
|
+
- **Deployer alert feed** — `client.deployerHunter.alerts()` pushes new deploys and graduations, tradability-filtered by default, with the tier resolved at read time.
|
|
142
|
+
- **Smart-money discovery** — `client.alphaWallets()` ranks trader wallets by realized net ETH, win rate, and memecoin share, flagging bot fleets and known KOLs.
|
|
143
|
+
- **Charting** — `client.tokens.candles()` returns 1-minute price + market-cap OHLC with buy/sell volume split.
|
|
144
|
+
|
|
145
|
+
## KOL trade intelligence — `client.kol`
|
|
146
|
+
|
|
147
|
+
### `client.kol.feed(params?)` — `GET /rhc/kol/feed` (BASIC)
|
|
148
|
+
|
|
149
|
+
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").
|
|
150
|
+
|
|
151
|
+
```ts
|
|
152
|
+
const { trades, next_before } = await client.kol.feed({
|
|
153
|
+
limit: 50, // 1–100
|
|
154
|
+
action: "buy", // "buy" | "sell"
|
|
155
|
+
kol: "0xabc…", // filter to one KOL's EVM wallet
|
|
156
|
+
min_eth: 0.25, // minimum trade size in ETH
|
|
157
|
+
// before: next_before, // cursor — page backwards
|
|
158
|
+
});
|
|
159
|
+
for (const t of trades) {
|
|
160
|
+
console.log(t.kol_name, "bought", t.token_symbol, `${t.eth_amount} ETH`, `${t.mc_multiple_since_trade}x since`);
|
|
161
|
+
}
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
Returns `RhcKolFeedResponse` — `{ chain, trades: RhcKolFeedTrade[], count, data_age_seconds, next_before }`.
|
|
165
|
+
|
|
166
|
+
### `client.kol.leaderboard(params?)` — `GET /rhc/kol/leaderboard` (BASIC)
|
|
167
|
+
|
|
168
|
+
KOLs ranked by trade count then net ETH flow over `24h` / `7d` / `30d`. `net_eth` is buy−sell flow (not realized PnL).
|
|
169
|
+
|
|
170
|
+
```ts
|
|
171
|
+
const { leaderboard } = await client.kol.leaderboard({ period: "7d", limit: 25 });
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
### `client.kol.hotTokens(params?)` — `GET /rhc/kol/hot-tokens` (BASIC)
|
|
175
|
+
|
|
176
|
+
Tokens bought by **2+ distinct KOLs** in the window (`5m`/`15m`/`1h`/`6h`/`24h`) — a consensus signal.
|
|
177
|
+
|
|
178
|
+
```ts
|
|
179
|
+
const { tokens } = await client.kol.hotTokens({ window: "1h" });
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
### `client.kol.coordination(params?)` — `GET /rhc/kol/coordination` (BASIC)
|
|
183
|
+
|
|
184
|
+
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).
|
|
185
|
+
|
|
186
|
+
```ts
|
|
187
|
+
const { coordination } = await client.kol.coordination({
|
|
188
|
+
period: "24h", // "1h" | "6h" | "24h" | "7d"
|
|
189
|
+
min_kols: 3, // 2–50
|
|
190
|
+
limit: 20, // 1–50
|
|
191
|
+
max_mc_usd: 250_000, // MC at the FIRST KOL buy (unknown entry MC is dropped when a band is set)
|
|
192
|
+
});
|
|
193
|
+
for (const c of coordination) {
|
|
194
|
+
console.log(c.token_symbol, c.kol_count, "KOLs", c.signal, `${c.net_eth} ETH net`, `${c.holders_count} still holding`);
|
|
195
|
+
}
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
RHC has no KOL winrate/strategy tables, so the Solana `avg_winrate_7d` / `coordination_score` fields are intentionally absent.
|
|
199
|
+
|
|
200
|
+
### `client.kol.firstTouches(params?)` — `GET /rhc/kol/first-touches` (BASIC)
|
|
201
|
+
|
|
202
|
+
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.
|
|
203
|
+
|
|
204
|
+
```ts
|
|
205
|
+
const { events, next_before } = await client.kol.firstTouches({
|
|
206
|
+
limit: 50, // 1–100 — clamped to 20 below PRO
|
|
207
|
+
token_age_max_min: 60, // only tokens under an hour old at first touch
|
|
208
|
+
min_eth: 0.1,
|
|
209
|
+
// since: lastSeen, // poll forward
|
|
210
|
+
// before: next_before, // page back
|
|
211
|
+
});
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
`first_kol.evm_address` is ULTRA-only; `name` and `twitter_url` are always returned.
|
|
215
|
+
|
|
216
|
+
### `client.kol.wallet(wallet)` — `GET /rhc/kol/{wallet}` (BASIC)
|
|
217
|
+
|
|
218
|
+
Aggregate stats over one KOL's last 200 RHC trades plus their 50 most recent.
|
|
219
|
+
|
|
220
|
+
```ts
|
|
221
|
+
const profile = await client.kol.wallet("0xabc…");
|
|
222
|
+
console.log(profile.kol_name, profile.stats.net_eth, "ETH net");
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
## DEX trade tape — `client.trades(params?)` — `GET /rhc/trades` (PRO+)
|
|
226
|
+
|
|
227
|
+
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`.
|
|
228
|
+
|
|
229
|
+
```ts
|
|
230
|
+
const { trades } = await client.trades({
|
|
231
|
+
token: "0xdef…", // filter to one token
|
|
232
|
+
dex: "uniswap-v3", // "uniswap-v2" | "uniswap-v3" | "uniswap-v4"
|
|
233
|
+
min_eth: 1,
|
|
234
|
+
limit: 100,
|
|
235
|
+
});
|
|
236
|
+
for (const t of trades) {
|
|
237
|
+
console.log(t.trader_eoa, t.action, t.eth_amount, "ETH", "gas", t.gas_price, "gwei", t.is_kol ? `(KOL ${t.kol_name})` : "");
|
|
238
|
+
}
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
## Liquidity removals — `client.lpEvents(params?)` — `GET /rhc/lp-events` (PRO+)
|
|
242
|
+
|
|
243
|
+
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()`).
|
|
244
|
+
|
|
245
|
+
```ts
|
|
246
|
+
const { events, has_more, next_before, coverage } = await client.lpEvents({ token: "0xdef…", limit: 50 });
|
|
247
|
+
for (const ev of events) {
|
|
248
|
+
if (ev.provider_is_token_deployer) console.warn("deployer pulled liquidity:", ev.tx_hash, ev.dex, ev.token_amount_raw);
|
|
249
|
+
}
|
|
250
|
+
console.log(coverage.adds_persisted); // false — always
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
> **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.
|
|
254
|
+
|
|
255
|
+
## Token intelligence — `client.tokens`
|
|
256
|
+
|
|
257
|
+
| Method | Endpoint | Tier | Returns |
|
|
258
|
+
|---|---|---|---|
|
|
259
|
+
| `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`. |
|
|
260
|
+
| `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. |
|
|
261
|
+
| `get(address)` | `/rhc/tokens/{address}` | BASIC | Full snapshot: metadata, price/MC/FDV, peak + drawdown, deployer reputation block, KOL activity, pool inventory. |
|
|
262
|
+
| `candles(address, params?)` | `/rhc/tokens/{address}/candles` | PRO+ | 1-minute price + market-cap OHLC, close liquidity, volume with buy/sell split, trade counts. |
|
|
263
|
+
| `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. |
|
|
264
|
+
| `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. |
|
|
265
|
+
| `bundle(address)` | `/rhc/tokens/{address}/bundle` | BASIC | Same-block launch-bundle detection + how much of what the cohort bought it still holds. |
|
|
266
|
+
| `batch(addresses)` | `POST /rhc/token/batch` | BASIC | Up to **50** tokens in one call — metadata, price/MC/FDV/liquidity, peak MC, deployer reputation. |
|
|
267
|
+
| `batchBuyerQuality(addresses)` | `POST /rhc/tokens/batch/buyer-quality` | BASIC | Up to **20** tokens' early-buyer quality scores in one call. |
|
|
268
|
+
| `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. |
|
|
269
|
+
| `flow(address, window?)` | `/rhc/tokens/{address}/flow` | PRO+ | Net buy/sell split by trader cohort — who is accumulating and who is distributing. |
|
|
270
|
+
| `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. |
|
|
271
|
+
| `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. |
|
|
272
|
+
| `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). |
|
|
273
|
+
|
|
274
|
+
### Tokenized stocks & ETFs — `equities(params?)` (BASIC)
|
|
275
|
+
|
|
276
|
+
```ts
|
|
277
|
+
const { equities, total_equities, identity } = await client.tokens.equities({ sort: "volume", limit: 20 });
|
|
278
|
+
for (const e of equities) {
|
|
279
|
+
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`);
|
|
280
|
+
}
|
|
281
|
+
const nvda = await client.tokens.equities({ symbol: "NVDA" }); // exact ticker, case-insensitive
|
|
282
|
+
console.log(identity.method, identity.issuer_beacon); // "beacon", 0xe10b…151b00
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
> **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`).
|
|
286
|
+
|
|
287
|
+
### Who is actually making money — `topTraders(address, params?)` (PRO+)
|
|
288
|
+
|
|
289
|
+
```ts
|
|
290
|
+
const { traders } = await client.tokens.topTraders("0xdef…", { limit: 25 });
|
|
291
|
+
for (const t of traders) {
|
|
292
|
+
console.log(t.trader_eoa, t.net_eth, t.win_rate, t.likely_bot ? "(bot)" : "");
|
|
293
|
+
}
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
> **`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()`.
|
|
297
|
+
|
|
298
|
+
### Who is buying vs dumping — `flow(address, window?)` (PRO+)
|
|
299
|
+
|
|
300
|
+
```ts
|
|
301
|
+
const { cohorts } = await client.tokens.flow("0xdef…", "24h");
|
|
302
|
+
// net_eth = sell − buy, so POSITIVE means that cohort DISTRIBUTED.
|
|
303
|
+
const bots = cohorts.find((c) => c.cohort === "bot");
|
|
304
|
+
const smart = cohorts.find((c) => c.cohort === "smart_money");
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
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.
|
|
308
|
+
|
|
309
|
+
### How far off the top — `peakHistory(address, params?)` (PRO+)
|
|
310
|
+
|
|
311
|
+
```ts
|
|
312
|
+
const p = await client.tokens.peakHistory("0xdef…", { window: "7d" });
|
|
313
|
+
console.log(p.peak.drawdown_from_peak, p.peak.peak_mc_usd_recorded, p.peak.peak_mc_usd_observed);
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
> **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.
|
|
317
|
+
|
|
318
|
+
### Can I actually sell this — `risk(address)` (PRO+)
|
|
319
|
+
|
|
320
|
+
```ts
|
|
321
|
+
const r = await client.tokens.risk("0xdef…");
|
|
322
|
+
if (r.sellability.sellable === "no") return; // bought-but-cannot-sell
|
|
323
|
+
if (r.flags.includes("upgradeable") || r.capabilities.can_mint) { /* treat with care */ }
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
> **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"`.
|
|
327
|
+
|
|
328
|
+
### Who holds it — `holders(address, params?)` (PRO+)
|
|
329
|
+
|
|
330
|
+
```ts
|
|
331
|
+
const h = await client.tokens.holders("0xdef…", { limit: 50 });
|
|
332
|
+
if (!h.verified) console.warn("unverified:", h.unverified_reason);
|
|
333
|
+
console.log(h.concentration?.top10_share, h.concentration?.pool_held_pct);
|
|
334
|
+
const g = h.holder_growth?.["24h"];
|
|
335
|
+
if (g) console.log(`24h: +${g.entered_still_holding} / -${g.exited} → net ${g.net}`);
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
> 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.
|
|
339
|
+
>
|
|
340
|
+
> **`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.
|
|
341
|
+
|
|
342
|
+
```ts
|
|
343
|
+
// Launch-bundle + quality gate before buying
|
|
344
|
+
const { bundle } = await client.tokens.bundle("0xdef…");
|
|
345
|
+
const quality = await client.tokens.buyerQuality("0xdef…");
|
|
346
|
+
if (bundle.bundle_kind === "same_block" && (bundle.held_pct_of_supply ?? 0) > 0.2 && !bundle.fully_exited) {
|
|
347
|
+
// bundle still sitting on supply — it can dump
|
|
348
|
+
}
|
|
349
|
+
if (quality.quality.signal === "negative") { /* skip */ }
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
> **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`).
|
|
353
|
+
|
|
354
|
+
### Batch reads
|
|
355
|
+
|
|
356
|
+
```ts
|
|
357
|
+
// Up to 50 tokens, one round-trip. Set-based server-side, not a fan-out of get().
|
|
358
|
+
// Every requested address is echoed back — unknown ones as { found: false } — so
|
|
359
|
+
// positions line up with what you sent.
|
|
360
|
+
const { tokens, requested, found } = await client.tokens.batch([token1, token2, token3]);
|
|
361
|
+
for (const t of tokens) {
|
|
362
|
+
if (t.found) console.log(t.symbol, t.market_cap_usd, t.deployer?.tier);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// Early-buyer quality for several tokens. MAX 20 — not the Solana batch cap of 50,
|
|
366
|
+
// because each token is a per-token cohort computation (early-buyer scan + bundle
|
|
367
|
+
// detection + alpha/cluster joins), so 50 would mean ~200 round-trips behind one
|
|
368
|
+
// request. The cap comes back as `max_addresses`. A token that fails to score
|
|
369
|
+
// degrades to an entry carrying `error` instead of failing the whole batch.
|
|
370
|
+
const { tokens: scored } = await client.tokens.batchBuyerQuality([token1, token2]);
|
|
371
|
+
for (const q of scored) {
|
|
372
|
+
if ("error" in q) console.warn(q.token_address, q.error);
|
|
373
|
+
else console.log(q.token_address, q.quality.score, q.quality.signal);
|
|
374
|
+
}
|
|
375
|
+
```
|
|
376
|
+
|
|
377
|
+
## Deployer reputation — `client.deployerHunter`
|
|
378
|
+
|
|
379
|
+
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`.
|
|
380
|
+
|
|
381
|
+
> **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.
|
|
382
|
+
|
|
383
|
+
| Method | Endpoint | Tier | Returns |
|
|
384
|
+
|---|---|---|---|
|
|
385
|
+
| `leaderboard(params?)` | `/rhc/deployer-hunter/leaderboard` | BASIC | 40k+ deployers ranked over a 5-min-refresh rollup. |
|
|
386
|
+
| `profile(address)` | `/rhc/deployer-hunter/{address}` | BASIC | Reputation row + 50 most recent tokens. |
|
|
387
|
+
| `trajectory(address)` | `/rhc/deployer-hunter/{address}/trajectory` | BASIC | Getting better or worse — streaks, rolling 10-launch success curve, trend, cadence. |
|
|
388
|
+
| `tokens(address, params?)` | `/rhc/deployer-hunter/{address}/tokens` | BASIC | Full paginated launch history with live + peak MC and liquidity. |
|
|
389
|
+
| `history(address, params?)` | `/rhc/deployer-hunter/{address}/history` | PRO+ | Deploy history + reputation row, exact `total`, `graduated_pool`. |
|
|
390
|
+
| `bestTokens(params?)` | `/rhc/deployer-hunter/best-tokens` | BASIC | Highest-peaking tokens from reputable (elite/good) deployers in a window. |
|
|
391
|
+
| `stats()` | `/rhc/deployer-hunter/stats` | BASIC | Chain-wide summary — population per tier, spam share, alert volume, active `tier_rules`. |
|
|
392
|
+
| `alerts(params?)` | `/rhc/deployer-hunter/alerts` | BASIC | New-deploy / graduation signal feed, tradability-filtered, read-time tier. |
|
|
393
|
+
| `recentBonds(params?)` | `/rhc/deployer-hunter/recent-bonds` | BASIC | Recent $40K graduations, newest peak first. |
|
|
394
|
+
|
|
395
|
+
```ts
|
|
396
|
+
// Leaderboard — 40k+ deployers, 5-min-refresh rollup
|
|
397
|
+
const { deployers, has_more } = await client.deployerHunter.leaderboard({
|
|
398
|
+
sort: "runner_rate", // graduation_rate | runner_rate | tokens_deployed | best_peak_mc_usd | last_deploy_at
|
|
399
|
+
tier: "elite",
|
|
400
|
+
min_tokens: 3,
|
|
401
|
+
limit: 20,
|
|
402
|
+
offset: 0,
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
// One deployer — unknown wallets return 200 with is_deployer:false (not a 404)
|
|
406
|
+
const { is_deployer, deployer, recent_tokens } = await client.deployerHunter.profile("0xabc…");
|
|
407
|
+
```
|
|
408
|
+
|
|
409
|
+
### Is this deployer improving? — `trajectory(address)` (BASIC)
|
|
410
|
+
|
|
411
|
+
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.
|
|
412
|
+
|
|
413
|
+
```ts
|
|
414
|
+
const { trajectory, success_metric, truncated } = await client.deployerHunter.trajectory("0xabc…");
|
|
415
|
+
console.log(trajectory?.trend, trajectory?.current_streak, success_metric);
|
|
416
|
+
```
|
|
417
|
+
|
|
418
|
+
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.
|
|
419
|
+
|
|
420
|
+
### Launch history — `tokens(address, params?)` (BASIC) and `history(address, params?)` (PRO+)
|
|
421
|
+
|
|
422
|
+
```ts
|
|
423
|
+
// Enumerable launch history with live MC, peak MC and liquidity
|
|
424
|
+
const { tokens, total, has_more, sort_scope } = await client.deployerHunter.tokens("0xabc…", {
|
|
425
|
+
limit: 50, // 1–100
|
|
426
|
+
offset: 0, // 0–10000
|
|
427
|
+
sort: "first_seen_at", // "first_seen_at" | "peak_mc_usd"
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
// PRO+ — the same history with graduated_pool and an exact total
|
|
431
|
+
const hist = await client.deployerHunter.history("0xabc…", { limit: 100, offset: 0 });
|
|
432
|
+
```
|
|
433
|
+
|
|
434
|
+
`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.
|
|
435
|
+
|
|
436
|
+
### Best tokens + chain stats
|
|
437
|
+
|
|
438
|
+
```ts
|
|
439
|
+
// What did the deployers worth tracking actually produce?
|
|
440
|
+
const { tokens, reputable_deployers, truncated } = await client.deployerHunter.bestTokens({
|
|
441
|
+
period: "7d", // "24h" | "7d" | "30d" | "all"
|
|
442
|
+
limit: 10, // 1–50
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
// The denominator for "is this deployer rare?"
|
|
446
|
+
const stats = await client.deployerHunter.stats();
|
|
447
|
+
console.log(stats.by_tier, stats.spam_token_share, stats.tier_rules.elite, stats.runner_definition);
|
|
448
|
+
```
|
|
449
|
+
|
|
450
|
+
`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.
|
|
451
|
+
|
|
452
|
+
### Deployer alerts — `alerts(params?)` (BASIC)
|
|
453
|
+
|
|
454
|
+
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.
|
|
455
|
+
|
|
456
|
+
```ts
|
|
457
|
+
const { alerts, tradability_filter, next_event_at } = await client.deployerHunter.alerts({
|
|
458
|
+
deployer_tier: "elite", // filters on the RESOLVED tier
|
|
459
|
+
alert_type: "new_deploy", // "new_deploy" | "graduated"
|
|
460
|
+
priority: "high", // "high" | "medium"
|
|
461
|
+
min_mc: 10_000,
|
|
462
|
+
limit: 50,
|
|
463
|
+
// include_untradeable: true, // opt out of the liquidity gate
|
|
464
|
+
});
|
|
465
|
+
for (const a of alerts) {
|
|
466
|
+
console.log(a.token_symbol, a.tier, a.liquidity_usd, a.tier_is_stale ? `(was ${a.tier_at_alert})` : "");
|
|
467
|
+
}
|
|
468
|
+
```
|
|
469
|
+
|
|
470
|
+
Two things worth knowing:
|
|
471
|
+
|
|
472
|
+
- **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`.
|
|
473
|
+
- **`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.
|
|
474
|
+
|
|
475
|
+
### Recent graduations — `recentBonds(params?)` (BASIC)
|
|
476
|
+
|
|
477
|
+
```ts
|
|
478
|
+
const { tokens, graduation_mc } = await client.deployerHunter.recentBonds({
|
|
479
|
+
deployer_tier: "good",
|
|
480
|
+
min_peak: 100_000, // only raises the $40K floor, never lowers it
|
|
481
|
+
limit: 50, // 1–200
|
|
482
|
+
});
|
|
483
|
+
```
|
|
484
|
+
|
|
485
|
+
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.
|
|
486
|
+
|
|
487
|
+
## Smart-money wallets — `client.alphaWallets(params?)` — `GET /rhc/alpha-wallets` (PRO+)
|
|
488
|
+
|
|
489
|
+
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.
|
|
490
|
+
|
|
491
|
+
```ts
|
|
492
|
+
const { wallets } = await client.alphaWallets({
|
|
493
|
+
classification: "smart_money", // all | human | bot | smart_money
|
|
494
|
+
identity: "unknown", // all | known_kol | unknown (net-new RHC smart money)
|
|
495
|
+
min_memecoin_share: 0.7,
|
|
496
|
+
sort: "net_eth", // net_eth | win_rate | trades | tokens | buy_eth | memecoin_share | last_trade_at
|
|
497
|
+
limit: 25,
|
|
498
|
+
});
|
|
499
|
+
```
|
|
500
|
+
|
|
501
|
+
## Rule engines — push, not polling
|
|
502
|
+
|
|
503
|
+
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.
|
|
504
|
+
|
|
505
|
+
### Copy-trade — `client.copyTrade` (PRO+)
|
|
506
|
+
|
|
507
|
+
```ts
|
|
508
|
+
const { subscription, webhook_secret } = await client.copyTrade.create({
|
|
509
|
+
name: "degen desk",
|
|
510
|
+
source_wallets: ["0xaaa…", "0xbbb…", "0xccc…"], // 1–250, per-tier cap enforced server-side
|
|
511
|
+
min_trade_eth: 0.01,
|
|
512
|
+
only_action: "buy", // buy | sell | both
|
|
513
|
+
sizing_mode: "fixed", // fixed | proportional | percent_source
|
|
514
|
+
sizing_amount: 0.05, // ETH when sizing_mode is "fixed"
|
|
515
|
+
delivery_mode: "websocket",
|
|
516
|
+
});
|
|
517
|
+
|
|
518
|
+
await client.copyTrade.update(subscription.id, { is_active: false });
|
|
519
|
+
await client.copyTrade.delete(subscription.id); // fired signals cascade
|
|
520
|
+
|
|
521
|
+
// Catch-up path for a missed webhook / dropped WS — fires retained 7 days
|
|
522
|
+
const since = new Date(Date.now() - 3_600_000).toISOString();
|
|
523
|
+
const { signals } = await client.copyTrade.signals({ subscription_id: subscription.id, since });
|
|
524
|
+
```
|
|
525
|
+
|
|
526
|
+
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.
|
|
527
|
+
|
|
528
|
+
### Price alerts — `client.priceAlerts` (PRO+)
|
|
529
|
+
|
|
530
|
+
```ts
|
|
531
|
+
const { alert, evaluation } = await client.priceAlerts.create({
|
|
532
|
+
token_address: "0xdef…", // must already be tracked on RHC with a market cap
|
|
533
|
+
drop_pct: 30, // 0.01–99.99, measured from the MC captured RIGHT NOW
|
|
534
|
+
recovery_pct: 15, // omit for a dip-only, terminal alert
|
|
535
|
+
webhook_url: "https://example.com/hook",
|
|
536
|
+
});
|
|
537
|
+
console.log(evaluation.mode, evaluation.trigger, evaluation.fallback_poll_seconds); // "event_driven", "rhc:dex_trade", { fast: 5, slow: 60 }
|
|
538
|
+
|
|
539
|
+
const { events } = await client.priceAlerts.events({ alert_id: alert.id, event_type: "dip" });
|
|
540
|
+
```
|
|
541
|
+
|
|
542
|
+
> **RHC price alerts are event-driven, but not sub-second like the Solana ones.** Since 2026-09-15 alerts are evaluated as trades land on the `rhc:dex_trade` feed, with a price-table poll (every 5 s while the feed is degraded or a trade carried no market cap, every 60 s otherwise) and a trade-tape replay after a feed outage as safety nets — latency is a few seconds (the chain trade flush is ~2 s). Every create response spells this out in its `evaluation` block (`mode: "event_driven"`, `trigger`, `fallback_poll_seconds`; `interval_seconds` is kept for compatibility). 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).
|
|
543
|
+
|
|
544
|
+
### KOL coordination rules — `client.kol.coordinationAlerts` (PRO+)
|
|
545
|
+
|
|
546
|
+
```ts
|
|
547
|
+
const { rule, scoring } = await client.kol.coordinationAlerts.create({
|
|
548
|
+
min_kols: 3, // 2–50 distinct tracked KOL buyers
|
|
549
|
+
window_minutes: 15, // 1–60 rolling window
|
|
550
|
+
min_score: 40, // 0–100
|
|
551
|
+
cooldown_min: 30, // 1–1440 before the same token can fire again
|
|
552
|
+
score_jump_break: 20, // score jump that breaks the cooldown early
|
|
553
|
+
delivery_mode: "websocket",
|
|
554
|
+
});
|
|
555
|
+
await client.kol.coordinationAlerts.update(rule.id, { min_kols: 4 }); // UUID id
|
|
556
|
+
```
|
|
557
|
+
|
|
558
|
+
> **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`.
|
|
559
|
+
|
|
560
|
+
### KOL first-touch subscriptions — `client.kol.firstTouchSubscriptions` (ULTRA+)
|
|
561
|
+
|
|
562
|
+
```ts
|
|
563
|
+
const { subscription } = await client.kol.firstTouchSubscriptions.create({
|
|
564
|
+
name: "early hands",
|
|
565
|
+
filters: {
|
|
566
|
+
min_first_buy_eth: 0.05,
|
|
567
|
+
min_kol_winrate: 0.5, // win-rate on CLOSED positions
|
|
568
|
+
strategy: "swing", // scalper | day_trader | swing | inactive | unscored
|
|
569
|
+
min_mc_usd: 10_000,
|
|
570
|
+
},
|
|
571
|
+
delivery_mode: "websocket",
|
|
572
|
+
});
|
|
573
|
+
|
|
574
|
+
// `filters` is a whole-object REPLACE, not a merge — {} clears every filter
|
|
575
|
+
await client.kol.firstTouchSubscriptions.update(subscription.id, { filters: {} });
|
|
576
|
+
```
|
|
577
|
+
|
|
578
|
+
> **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.
|
|
579
|
+
|
|
580
|
+
## Streaming — `client.stream` (PRO+)
|
|
581
|
+
|
|
582
|
+
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). Nine RHC channels:
|
|
583
|
+
|
|
584
|
+
| Channel | Emits | Tier | Scope |
|
|
585
|
+
|---|---|---|---|
|
|
586
|
+
| `rhc:kol_trades` | `rhc:kol_trade` | PRO+ | broadcast — the live KOL tape |
|
|
587
|
+
| `rhc:dex_trades` | `rhc:dex_trade` | **ULTRA+** | broadcast — the full DEX firehose |
|
|
588
|
+
| `rhc:dex_trades_unattributed` | `rhc:dex_trade_unattributed` | **ULTRA+** | broadcast — trades on pools with no single "token" side (e.g. WETH/USDG); subscribe with `rhc:dex_trades` for full coverage |
|
|
589
|
+
| `rhc:new_tokens` | `rhc:new_token` | **ULTRA+** | broadcast — a token's symbol/name/decimals resolved for the first time |
|
|
590
|
+
| `rhc:copytrade:signals` | `rhc:copytrade:signal` | PRO+ | user-scoped — only **your** rules' fires |
|
|
591
|
+
| `rhc:price_alert:events` | `rhc:price_alert:dip`, `rhc:price_alert:recovery` | PRO+ | user-scoped; event-driven off each trade (a few seconds), not sub-second |
|
|
592
|
+
| `rhc:kol:coordination` | `rhc:kol:coordination` | PRO+ | user-scoped — only **your** rules' fires |
|
|
593
|
+
| `rhc:kol:first_touches` | `rhc:kol:first_touch` | PRO+ | broadcast — ULTRA gates only the first-touch *subscription CRUD*, not this channel |
|
|
594
|
+
| `rhc:token_locks` | `rhc:token_lock` | PRO+ | broadcast — a token lock / vesting contract created on chain |
|
|
595
|
+
|
|
596
|
+
> **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`.
|
|
597
|
+
|
|
598
|
+
```ts
|
|
599
|
+
const stream = client.stream.connect();
|
|
600
|
+
|
|
601
|
+
stream
|
|
602
|
+
.on("open", () => console.log("connected"))
|
|
603
|
+
.on("rhc:kol_trade", (trade) => console.log("KOL trade", trade))
|
|
604
|
+
.on("rhc:dex_trade", (trade) => console.log("DEX trade", trade))
|
|
605
|
+
// New in 0.5.0 — the server tells you when a channel was refused (typo or
|
|
606
|
+
// tier gate); 0.4.0 dropped this frame and the stream just stayed silent.
|
|
607
|
+
.on("warning", (w) => console.warn("rejected:", w.code, w.rejected, w.valid_channels))
|
|
608
|
+
.on("error", (err) => console.error(err));
|
|
609
|
+
|
|
610
|
+
stream.subscribe(["rhc:kol_trades", "rhc:dex_trades"]);
|
|
611
|
+
// …later
|
|
612
|
+
stream.close(); // clean shutdown — short-lived scripts exit promptly
|
|
613
|
+
```
|
|
614
|
+
|
|
615
|
+
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 })`.
|
|
616
|
+
|
|
617
|
+
### Recovery: cursor, resume, de-duplication *(new in 0.9.0)*
|
|
618
|
+
|
|
619
|
+
The stream client keeps a **resume cursor** `{ instance, seq, ts }` — the position of the last frame your handlers finished — and on every reconnect asks the server to resume after it (`subscribe { …, resume }`).
|
|
620
|
+
|
|
621
|
+
- **"Processed"** means every handler for that frame returned, or the promise it returned settled. Return a promise from an async handler and the cursor waits for it (and for every earlier frame). A handler that throws or rejects still counts as processed; the error goes to `error`.
|
|
622
|
+
- **At-least-once, never exactly-once.** After a reconnect a frame can arrive again. The client drops ids it delivered recently (the last 10,000, option `dedupeSize`); anything you persist should still dedupe on `evt.id`. Replayed frames carry `evt.replayed === true`.
|
|
623
|
+
- **Persistence.** The cursor lives in memory. Save `stream.getCursor()` (or on every `cursor` event) and pass it back as `{ resume }` to continue after a process restart. Persist the committed cursor, never `getProgress()`.
|
|
624
|
+
- **Committed cursor vs progress.** `getCursor()` is the COMMITTED, safe cursor — persist and resume from this one. `getProgress()` is what has been received and handled (replayed frames included) and is not safe to resume from. Live frames commit as they are handled. Replayed frames never commit: the server replays channel by channel, so only a `replay_end` the server calls complete — or one whose gaps are all final — commits, at the server's `last_seq` / `last_ts`. If a recovery is incomplete, or the socket closes mid-replay, the committed cursor stays at the pre-resume point, and live frames after it are delivered but not committed until a later recovery completes (`isRecoveryIncomplete()`). **Trade-off:** the next reconnect re-requests the unrecovered range from the old cursor, and what arrives twice is dropped by id. Call `acceptGap()` once you have backfilled the range the `gap` event named, or decided to skip it. A gap the server calls final is handled by `onUnrecoverableGap` (below).
|
|
625
|
+
- **Gaps: what is known, and who decides.** A `gap` event says which channels the server could not rebuild, the **range that may be incomplete** (`skipped.from` → `skipped.to`, plus `from`), the server's `reason`, whether it is `permanent`, and the bounds the server reported (`limits`, and per-channel `time_basis` / `truncated_at_ts` / `retry_after_ms` under `channels`). Events in that range **may** be missing — the number cannot be known, so it is never stated. Backfill the range from REST if you need certainty.
|
|
626
|
+
- **Transient** (`retryable: true` — `backpressure`, `closed`, `source_busy`, `source_error`, `late_ingest_possible`, `row_cap`): the committed cursor stays put, live frames do not commit, and the client resumes again after the server's `retryAfterMs` (for `row_cap`, from `resumeTsHint`), then on every reconnect. `resumeTsHint` is used only when every incomplete **retryable** channel is `row_cap` (a channel whose gap is final does not block it, and is still reported); otherwise the retry asks from the committed cursor again. The client asks again after the server’s `retryAfterMs` at most `maxResumeRetries` times per connection (default 5; the budget resets on every reconnect); when that budget is spent the gap event says `exhausted: true`, the cursor stays where it is, and the next reconnect resumes again.
|
|
627
|
+
- **Final** (`retryable: false` — `not_reconstructable`, `window_exceeded`, and an older server's `ring_truncated` / `instance_changed`): asking again can never fill it. **The SDK then decides to continue** — that is the client's decision, not your approval — and reports it on the same `gap` event with `advancedPastGap: true`, `source: "auto"` and the range being skipped, *before* the cursor moves. Set `onUnrecoverableGap: "stop"` to keep the cursor instead: the stream stops and emits `fatal` with the gap, and you decide (`acceptGap()` then `connect()` continues; `acceptGap()` reports the same gap with `source: "manual"`).
|
|
628
|
+
- **Older servers.** Against a server that does not understand `resume` yet, the client falls back to `replay_since_seq` (same server process) or `replay_since_ts` (the server restarted). That only covers the server's in-memory buffer (minutes), and a restart is reported as a `gap` with `instance_changed`.
|
|
629
|
+
- **Close codes.** `4001` → the token is re-fetched and the client reconnects (`maxAuthRetries`, default 3, then `fatal`); `4002` connection limit → `error` plus a wait of at least 60 s (`connectionLimitBackoffMs`) — free a ghost slot with the stream-sessions API; `4003` → `fatal`, the client stops; `4008` slow consumer → reconnect and resume. The backoff resets only when the server acks a subscribe, never on a bare socket open.
|
|
630
|
+
- **Warnings.** `warning` fires for every server warning frame, including `channels_rejected` and `channels_revoked` (revoked channels are removed from the subscription so reconnects do not re-request them) (a channel dropped after a plan change). A rejected or revoked channel is silent, so handle it.
|
|
631
|
+
|
|
632
|
+
```ts
|
|
633
|
+
const stream = client.stream.connect({ resume: loadCursor() ?? undefined });
|
|
634
|
+
stream.on("*", async (data, evt) => {
|
|
635
|
+
await store.upsert(evt!.id, data); // the cursor advances once this resolves
|
|
636
|
+
});
|
|
637
|
+
stream.on("cursor", (c) => saveCursor(c)); // { instance, seq, ts }
|
|
638
|
+
stream.on("gap", (g) => console.warn("may be missing:", g.reasons, g.skipped)); // g.advancedPastGap: the SDK continued past it
|
|
639
|
+
stream.on("fatal", (f) => console.error("stream stopped:", f.code, f.reason));
|
|
640
|
+
```
|
|
641
|
+
|
|
642
|
+
## Error handling
|
|
643
|
+
|
|
644
|
+
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.
|
|
645
|
+
|
|
646
|
+
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.
|
|
647
|
+
|
|
648
|
+
```ts
|
|
649
|
+
import { RobinhoodError } from "robinhood-chain-sdk";
|
|
650
|
+
|
|
651
|
+
try {
|
|
652
|
+
await client.trades({ limit: 100 }); // PRO+
|
|
653
|
+
} catch (err) {
|
|
654
|
+
if (err instanceof RobinhoodError) {
|
|
655
|
+
if (err.status === 403) console.error("Upgrade required:", err.message);
|
|
656
|
+
else console.error(err.status, err.requestId, err.message);
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
```
|
|
660
|
+
|
|
661
|
+
## Types & constants
|
|
662
|
+
|
|
663
|
+
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`).
|
|
664
|
+
|
|
665
|
+
## Links
|
|
666
|
+
|
|
667
|
+
- **Robinhood Chain** — [madeonsol.com/robinhood](https://madeonsol.com/robinhood)
|
|
668
|
+
- **Pricing & free API key** — [madeonsol.com/pricing](https://madeonsol.com/pricing) (Robinhood Chain bundled into every tier)
|
|
669
|
+
- **API docs** — [madeonsol.com/api-docs](https://madeonsol.com/api-docs)
|
|
670
|
+
- **npm** — [robinhood-chain-sdk](https://www.npmjs.com/package/robinhood-chain-sdk)
|
|
671
|
+
- **GitHub** — [madeonsol/robinhood-chain-sdk](https://github.com/madeonsol/robinhood-chain-sdk)
|
|
672
|
+
|
|
673
|
+
## License
|
|
674
|
+
|
|
675
|
+
MIT © MadeOnSol
|