# robinhood-chain
[](https://crates.io/crates/robinhood-chain)
[](https://docs.rs/robinhood-chain)
[](https://crates.io/crates/robinhood-chain)
[](LICENSE)
> β **[Star on GitHub](https://github.com/madeonsol/robinhood-chain-rs)** Β· π **[Examples](./examples/)** Β· π **[docs.rs](https://docs.rs/robinhood-chain)** Β· π **[Robinhood Chain](https://madeonsol.com/robinhood)**
**Robinhood Chain SDK for Rust β EVM-native trading intelligence, chain id 4663.**
Typed, async, `tokio`-based, `rustls`-only client for the [MadeOnSol](https://madeonsol.com) Robinhood Chain API: live KOL trades, token discovery, launch-bundle detection, deployer reputation, smart-money wallet ranking, 1-minute OHLC candles, the DEX trade tape, and four push **rule engines** (copy-trade, price alerts, KOL coordination, KOL first-touches) β all served from our self-hosted Robinhood Chain node.
Robinhood Chain is an **Arbitrum Orbit L2**, so every field is EVM-native β `token_address` (lowercase `0xβ¦`), `eth_amount`, `tx_hash`, `block_number`, `net_flow_eth`. No Solana field names.
> Robinhood Chain coverage is **bundled into every MadeOnSol tier at no extra cost** β same `msk_` API key, same base URL (`https://madeonsol.com/api/v1`). Get a free key at **<https://madeonsol.com/pricing>**.
> **New in 0.8.1 β fix: stream tokens never expire, and `StreamToken` deserializes again.** Since 2026-08-27 `POST /stream/token` returns the SAME token on every call and it never expires β it stops working only if your subscription lapses or you replace it with `{"rotate": true}` (the old value keeps working for 60 s). The API now sends `expires_at: null` and `next_refresh_at: null`, which 0.8.0's `StreamToken { expires_at: String }` refused to deserialize, so `client.stream.get_token()` errored for every caller. **`StreamToken.expires_at` is now `Option<String>`** (always `None`; kept for wire compatibility β do not schedule refreshes on it), `next_refresh_at` stays `Option<String>` (always `None`), and two fields are new: `rotated: Option<bool>` (`Some(true)` when the call replaced an existing token) and `lifetime: Option<String>` (the server's plain-English statement of the above). New method **`client.stream.rotate_token()`** sends `{"rotate": true}` for the leaked-token case. A WebSocket close code `4001` means "call `get_token()` again and reconnect", never "the token timed out".
> **New in 0.8.0 β tokenized equities + the liquidity-removals feed.** Two new methods. `client.tokens.equities(¶ms)` (`GET /rhc/equities`, **BASIC+**, `types::EquitiesParams` β `types::EquitiesResponse`) lists every official Robinhood tokenized stock and ETF (NVDA, SPY, AAPL, β¦) with live price / MC / liquidity and 24h trades, ETH volume and buyer-seller split, sortable with `types::EquitiesSort` (`Volume` default / `Trades` / `MarketCap` / `LastTrade` / `Symbol`), filterable by exact `symbol` or substring `q`. **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 `0xe10b6f6bβ¦151b00`, 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 of them appear. `client.trades.lp_events(¶ms)` (`GET /rhc/lp-events`, **PRO+**, `types::LpEventsParams` β `types::LpEventsResponse`) is the rug signal: Uniswap v2/v3 `Burn` and v4 `ModifyLiquidity` with a negative delta on tracked pools, from our node's log subscription, filterable by `token` / `pool` / `provider` / `dex` and cursor-paginated on `next_before`. **Removals only** β adds are not persisted (`coverage.adds_persisted == false`), amounts (`liquidity`, `amount0`, `amount1`, `token_amount_raw`, `quote_amount_raw`) are raw uint256 `String`s, v4 rows carry `liquidity` only, and `provider_is_token_deployer` is the classic rug tell. Data since 2026-08-05.
> **New in 0.7.0 β `holder_growth`: who arrived and who left.** `client.tokens().holders(address, ¶ms)` 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. Deserialize it with `serde_json::from_value::<Option<types::HolderGrowth>>(resp["holder_growth"].clone())`.
> **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()`, `tracked_trades()` and `tracked_summary()`. 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.
## Install
```toml
[dependencies]
robinhood-chain = "0.8"
tokio = { version = "1", features = ["macros", "rt-multi-thread"] }
```
Requires Rust 1.75+. Uses `reqwest` with `rustls-tls` (no OpenSSL dependency).
## Quick start
```rust
use robinhood_chain::{RobinhoodChain, types::{KolFeedParams, TradeAction}};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// Free key β RHC is bundled into every tier. https://madeonsol.com/pricing
let client = RobinhoodChain::new(std::env::var("MADEONSOL_API_KEY")?)?;
let feed = client.kol.feed(&KolFeedParams {
limit: Some(10),
action: Some(TradeAction::Buy),
..Default::default()
}).await?;
for t in feed.trades {
println!("{:?} bought {:?} for {} ETH (ran {:?}Γ)",
t.kol_name, t.token_symbol,
t.eth_amount.unwrap_or(0.0), t.mc_multiple_since_trade);
}
Ok(())
}
```
Run the bundled examples:
```sh
export MADEONSOL_API_KEY=msk_...
cargo run --example kol_feed
cargo run --example deployer_leaderboard
```
## Namespaces
The `RobinhoodChain` client exposes namespaced sub-clients:
| Namespace | Purpose |
|---|---|
| `client.kol` | KOL feed, activity leaderboard, consensus hot-tokens, coordination, first-touches, single-KOL profile β plus the coordination-alert (PRO+) and first-touch-subscription (ULTRA+) rule engines |
| `client.trades` | The Robinhood Chain DEX trade tape (Uniswap v2/v3/v4) + the liquidity-removals feed (`lp_events`) |
| `client.tokens` | Token discovery, beacon-verified tokenized equities, per-token snapshot, OHLC candles, KOL-consensus, buyer-quality, launch bundle, top-traders, flow, peak-history, risk, holders, batch reads |
| `client.deployer_hunter` | Deployer reputation: leaderboard, profile, trajectory, launch history, best-tokens, chain-wide stats, alerts, recent graduations |
| `client.alpha_wallets` | Smart-money wallet ranking |
| `client.copytrade` | Copy-trade rule engine: rules + fired-signal history (PRO+) |
| `client.price_alerts` | Price-alert rule engine: alerts + dip/recovery events (PRO+) |
| `client.stream` | WebSocket streaming token issuance (**non-expiring since 2026-08-27**) + rotation *(new 0.8.1)* + the six `rhc:*` channels (see [Streaming](#streaming)) |
## Endpoint β method map (54 operations listed, 42 paths β plus the 10 `client.wallet` operations documented in the 0.6.0 note above)
> The table previously claimed "all 25 routes" β it had silently omitted the five
> token-intel endpoints added in 0.3.0 (`top-traders`, `flow`, `peak-history`,
> `risk`, `holders`). Both the count and those rows are corrected here, and the
> CI route-parity guard now pins the full set.
| # | Endpoint | Method | Tier |
|---|---|---|---|
| 1 | `GET /rhc/kol/feed` | `client.kol.feed(¶ms)` | BASIC+ |
| 2 | `GET /rhc/kol/leaderboard` | `client.kol.leaderboard(¶ms)` | BASIC+ |
| 3 | `GET /rhc/kol/hot-tokens` | `client.kol.hot_tokens(¶ms)` | BASIC+ |
| 4 | `GET /rhc/kol/coordination` | `client.kol.coordination(¶ms)` | BASIC+ |
| 5 | `GET /rhc/kol/first-touches` | `client.kol.first_touches(¶ms)` | BASIC+ |
| 6 | `GET /rhc/kol/{wallet}` | `client.kol.wallet(addr)` | BASIC+ |
| 7 | `GET /rhc/trades` | `client.trades.list(¶ms)` | PRO+ |
| 8 | `GET /rhc/lp-events` | `client.trades.lp_events(¶ms)` β liquidity **removals** only (rug signal); raw uint256 strings; `types::LpEventsParams` | PRO+ |
| 9 | `GET /rhc/tokens` | `client.tokens.list(¶ms)` | PRO+ |
| 10 | `GET /rhc/equities` | `client.tokens.equities(¶ms)` β beacon-verified tokenized stocks/ETFs; `types::EquitiesParams` / `types::EquitiesSort` | BASIC+ |
| 11 | `GET /rhc/tokens/{address}` | `client.tokens.get(addr)` | BASIC+ |
| 12 | `GET /rhc/tokens/{address}/candles` | `client.tokens.candles(addr, ¶ms)` | PRO+ |
| 13 | `GET /rhc/tokens/{address}/kol-consensus` | `client.tokens.kol_consensus(addr)` | PRO+ |
| 14 | `GET /rhc/tokens/{address}/buyer-quality` | `client.tokens.buyer_quality(addr)` | BASIC+ |
| 15 | `GET /rhc/tokens/{address}/bundle` | `client.tokens.bundle(addr)` | BASIC+ |
| 16 | `GET /rhc/tokens/{address}/top-traders` | `client.tokens.top_traders(addr, ¶ms)` | PRO+ |
| 17 | `GET /rhc/tokens/{address}/flow` | `client.tokens.flow(addr, ¶ms)` | PRO+ |
| 18 | `GET /rhc/tokens/{address}/peak-history` | `client.tokens.peak_history(addr, ¶ms)` | PRO+ |
| 19 | `GET /rhc/tokens/{address}/risk` | `client.tokens.risk(addr)` | PRO+ |
| 20 | `GET /rhc/tokens/{address}/holders` | `client.tokens.holders(addr, ¶ms)` β includes `holder_growth` (1h/24h/7d entered / exited / net; `types::HolderGrowth`) | PRO+ |
| 21 | `POST /rhc/token/batch` | `client.tokens.batch(&addresses)` | BASIC+ |
| 22 | `POST /rhc/tokens/batch/buyer-quality` | `client.tokens.batch_buyer_quality(&addresses)` | BASIC+ |
| 23 | `GET /rhc/deployer-hunter/leaderboard` | `client.deployer_hunter.leaderboard(¶ms)` | BASIC+ |
| 24 | `GET /rhc/deployer-hunter/{address}` | `client.deployer_hunter.profile(addr)` | BASIC+ |
| 25 | `GET /rhc/deployer-hunter/{address}/trajectory` | `client.deployer_hunter.trajectory(addr)` | BASIC+ |
| 26 | `GET /rhc/deployer-hunter/{address}/tokens` | `client.deployer_hunter.tokens(addr, ¶ms)` | BASIC+ |
| 27 | `GET /rhc/deployer-hunter/{address}/history` | `client.deployer_hunter.history(addr, ¶ms)` | PRO+ |
| 28 | `GET /rhc/deployer-hunter/best-tokens` | `client.deployer_hunter.best_tokens(¶ms)` | BASIC+ |
| 29 | `GET /rhc/deployer-hunter/recent-bonds` | `client.deployer_hunter.recent_bonds(¶ms)` | BASIC+ |
| 30 | `GET /rhc/deployer-hunter/stats` | `client.deployer_hunter.stats()` | BASIC+ |
| 31 | `GET /rhc/deployer-hunter/alerts` | `client.deployer_hunter.alerts(¶ms)` | BASIC+ |
| 32 | `GET /rhc/alpha-wallets` | `client.alpha_wallets.list(¶ms)` | PRO+ |
| 33 | `GET /rhc/copytrade/subscriptions` | `client.copytrade.list()` | PRO+ |
| 34 | `POST /rhc/copytrade/subscriptions` | `client.copytrade.create(¶ms)` | PRO+ |
| 35 | `GET /rhc/copytrade/subscriptions/{id}` | `client.copytrade.get(id)` | PRO+ |
| 36 | `PATCH /rhc/copytrade/subscriptions/{id}` | `client.copytrade.update(id, ¶ms)` | PRO+ |
| 37 | `DELETE /rhc/copytrade/subscriptions/{id}` | `client.copytrade.delete(id)` | PRO+ |
| 38 | `GET /rhc/copytrade/signals` | `client.copytrade.signals(¶ms)` | PRO+ |
| 39 | `GET /rhc/price-alerts` | `client.price_alerts.list()` | PRO+ |
| 40 | `POST /rhc/price-alerts` | `client.price_alerts.create(¶ms)` | PRO+ |
| 41 | `GET /rhc/price-alerts/{id}` | `client.price_alerts.get(id)` | PRO+ |
| 42 | `PATCH /rhc/price-alerts/{id}` | `client.price_alerts.update(id, ¶ms)` | PRO+ |
| 43 | `DELETE /rhc/price-alerts/{id}` | `client.price_alerts.delete(id)` | PRO+ |
| 44 | `GET /rhc/price-alerts/events` | `client.price_alerts.events(¶ms)` | PRO+ |
| 45 | `GET /rhc/kol/coordination/alerts` | `client.kol.coordination_alerts_list()` | PRO+ |
| 46 | `POST /rhc/kol/coordination/alerts` | `client.kol.coordination_alerts_create(¶ms)` | PRO+ |
| 47 | `GET /rhc/kol/coordination/alerts/{id}` | `client.kol.coordination_alerts_get(uuid)` | PRO+ |
| 48 | `PATCH /rhc/kol/coordination/alerts/{id}` | `client.kol.coordination_alerts_update(uuid, ¶ms)` | PRO+ |
| 49 | `DELETE /rhc/kol/coordination/alerts/{id}` | `client.kol.coordination_alerts_delete(uuid)` | PRO+ |
| 50 | `GET /rhc/kol/first-touches/subscriptions` | `client.kol.first_touch_subscriptions_list()` | ULTRA+ |
| 51 | `POST /rhc/kol/first-touches/subscriptions` | `client.kol.first_touch_subscriptions_create(¶ms)` | ULTRA+ |
| 52 | `GET /rhc/kol/first-touches/subscriptions/{id}` | `client.kol.first_touch_subscriptions_get(uuid)` | ULTRA+ |
| 53 | `PATCH /rhc/kol/first-touches/subscriptions/{id}` | `client.kol.first_touch_subscriptions_update(uuid, ¶ms)` | ULTRA+ |
| 54 | `DELETE /rhc/kol/first-touches/subscriptions/{id}` | `client.kol.first_touch_subscriptions_delete(uuid)` | ULTRA+ |
`BASIC+` = any valid key (including the free tier β note free keys read live feeds like the KOL tape on a 5-minute delay; paid tiers are real-time). `PRO+` = Pro or Ultra.
`ULTRA+` = Ultra or Business. Some BASIC+ endpoints return richer field-gated
payloads on higher tiers (e.g. the launch-`bundle` cohort: BASIC gets the scalar
signal, PRO the top-10 wallets, ULTRA the full cohort with alpha-wallet
identity).
Copy-trade and price-alert ids are `i64`; coordination-alert and first-touch
subscription ids are UUID `&str`.
## What the data is
- **KOL feed / leaderboard / hot-tokens** β every buy/sell from tracked Solana KOLs' verified EVM wallets on Robinhood Chain, attributed to the effective trading account (`tx.from`, or the ERC-4337 userOp sender when the trade was bundled). The KOLβEVM mapping is recovered by tracing each KOL's SolanaβEVM bridge deposits (deBridge / Relay / Mayan / Wormhole) β a dataset unique to MadeOnSol. Hot-tokens surfaces tokens bought by 2+ distinct KOLs (a consensus signal).
- **KOL coordination + first-touches** β `coordination` goes a level deeper than hot-tokens: the cohort *composition* behind each consensus token (per-KOL buy/sell legs, `accumulating` vs `distributing`, `exited_count`, `time_to_consensus_sec`). `first_touches` is the earliest KOL entry per token β the discovery signal, with the MC at entry vs current/peak so you can score how the call aged.
- **Trade tape** (`client.trades`) β every Uniswap v2/v3/v4 swap on chain 4663, ~sub-second from execution, carrying the effective trading account (`trader_eoa` β `tx.from`, or the ERC-4337 userOp sender when bundled; never the router or the bundler), gas/ordering for MEV analysis, and KOL/deployer flags.
- **Liquidity removals** (`client.trades.lp_events`) β the rug signal: v2/v3 `Burn` + v4 negative `ModifyLiquidity` on tracked pools, from our node. **Removals only** (adds are not persisted β `coverage.adds_persisted == false`), raw uint256 amounts as `String`s, `provider_is_token_deployer` flags the deployer pulling its own pool. Data since 2026-08-05.
- **Token discovery + snapshot** β live-priced tokens with MC, liquidity, peak MC + drawdown, launchpad (pons / flap / clanker / hood.fun / noxa / virtuals), and deployer reputation tier.
- **Tokenized equities** (`client.tokens.equities`) β every official Robinhood tokenized stock / ETF with live price / MC / liquidity + 24h trades, ETH volume and buyer-seller split. Identity is the issuer **beacon** (`0xe10b6f6bβ¦151b00`, read from our node), never the name β look-alike "GameStop β’ Robinhood Token" contracts never appear; `issuer_beacon` is echoed per row.
- **Batch reads** β `tokens.batch()` resolves up to **50** tokens in one set-based call (three server-side queries regardless of batch size), echoing every requested address back so positions line up (`found: false` for unknowns). `tokens.batch_buyer_quality()` caps at **20**, deliberately: buyer-quality is a per-token cohort computation, not one set-based query, and a per-token failure degrades to an `error` entry instead of failing the batch.
- **Buyer-quality + bundle** β a 0β100 quality read on a token's first-20 buyer cohort, and same-block launch-bundle detection with current-held %. (No `atomic_tx` kind β RHC is an L2 with no atomic multi-signer tx, so a detected bundle is `same_block`.)
- **Deployer reputation** β 40k+ deployers with tier, trajectory (streaks, rolling 10-launch success rate, `improving`/`declining`/`stable`), full paginated launch history, and chain-wide stats. Most RHC launchpads are direct-to-DEX (no bonding curve), so "graduation" is a $40K+ peak-MC milestone and a "runner" reached $100K+.
- **Deployer alerts** β live signals when a tracked deployer ships a new token or one of their tokens graduates.
- **Alpha wallets** β the reverse of KOL discovery: RHC trader wallets ranked by realized on-chain performance (`net_eth`, `win_rate`, `memecoin_share`, `likely_bot`).
- **Token intel** β per-token `top_traders` (realized flow, not PnL β a holder ranks last), `flow` by trader cohort (`net_eth = sell β buy`, so positive means distribution), `peak_history` (two peaks, because recorded and observed disagree), EVM-native `risk` (sellability simulated at the chain head, never cached), and exact `holders` folded from `Transfer` logs β including `holder_growth` (`1h` / `24h` / `7d`: `entered`, `entered_still_holding`, `exited`, `net` β Ξ `holder_count`; pools and burns excluded, a window is `null` only when the chain had no ingested trades in it). Deserialize it with `serde_json::from_value::<Option<types::HolderGrowth>>(h["holder_growth"].clone())`.
- **Rule engines** β copy-trade, price alerts, KOL coordination and KOL first-touches, each pushing to a webhook or WebSocket with a queryable fire history. See [Rule engines](#rule-engines-push).
### Two things to know about deployer tiers
**Tiers ride the $100K runner rate, not the $40K graduation rate.** `elite` and `good` are earned on `runner_rate` and require 24h of deployer history β the $40K bar proved farmable by operators mass-relaunching one ticker across rotating wallets. `graduation_rate` is still returned and still means the $40K bar, but it no longer determines the tier; `spammer` is the one exception that still keys off it. `client.deployer_hunter.stats()` returns the live thresholds in `tier_rules`, so you never have to guess what `elite` currently means.
**Alerts filter for tradability by default, and resolve the tier at read time.** Alerts whose token has `liquidity_usd` below $100 β or unknown liquidity, which on RHC usually means a drained pool β are dropped: a $45K-MC alert on a token with $68 of liquidity is not a signal. Pass `include_untradeable: Some(true)` for the raw tape; the active setting comes back as `tradability_filter`. Each alert's `tier` is the deployer's *current* tier, with the snapshot taken when the alert fired alongside as `tier_at_alert` and `tier_is_stale` set when they disagree.
```rust
use robinhood_chain::types::{DeployerAlertsParams, DeployerTier};
let alerts = client.deployer_hunter.alerts(&DeployerAlertsParams {
deployer_tier: Some(DeployerTier::Elite),
limit: Some(25),
..Default::default()
}).await?;
for a in alerts.alerts {
println!("{:?} {:?} β liquidity ${:?} (stale tier: {})",
a.alert_type, a.token_symbol, a.liquidity_usd, a.tier_is_stale);
}
```
## Rule engines (push)
Four engines turn the read endpoints into push. Each rule delivers by webhook
(HMAC-SHA256 over `<timestamp>.<body>` in `X-MadeOnSol-Signature`), WebSocket, or
both, and each keeps a queryable fire history so you can catch up after a missed
delivery.
| Engine | Namespace | Fires when | History | Tier |
|---|---|---|---|---|
| Copy-trade | `client.copytrade` | a wallet you follow trades on RHC | `signals()`, 7 days | PRO+ |
| Price alerts | `client.price_alerts` | a token drops (and optionally bounces back) | `events()`, 30 days | PRO+ |
| KOL coordination | `client.kol.coordination_alerts_*` | N+ tracked KOLs buy the same token in a window | via WS / webhook | PRO+ |
| KOL first touches | `client.kol.first_touch_subscriptions_*` | a token gets its FIRST tracked-KOL buy | via WS / webhook | ULTRA+ |
**Quotas are per chain.** A full set of Solana rules consumes no Robinhood Chain
capacity, and vice versa.
**Three RHC-specific differences before you port Solana code:**
- **Price alerts are ~15s POLLED, not sub-second.** The RHC price writer emits no
`pg_notify`, so alerts are polled off `rhc_token_prices`; effective latency is
that interval plus the token's own price-update cadence. Every create response
says so in its `evaluation` block.
- **Copy-trade has no market-cap band.** The RHC notify payload 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. Sizes are ETH, not SOL.
- **First-touch filters are strict and smaller.** RHC has no scout score, so
`min_scout_tier` / `min_n_touches` are *absent* rather than silently matching
nothing β `min_kol_winrate` and `strategy` are the quality gates, and unknown
filter keys are rejected with a 400.
```rust
use robinhood_chain::types::*;
let created = client.copytrade.create(&CopyTradeCreateParams {
name: Some("whale follow".into()),
source_wallets: vec!["0xabcβ¦".into()],
sizing_mode: Some(CopyTradeSizingMode::Proportional),
sizing_amount: 0.5,
delivery_mode: Some(DeliveryMode::Websocket),
..Default::default()
}).await?;
// `webhook_secret` is minted once (whenever a webhook_url was supplied) β store it now.
println!("rule {} β {}", created.subscription.id, created.note);
// Catch up on anything the socket missed.
let fired = client.copytrade.signals(&CopyTradeSignalsParams {
subscription_id: Some(created.subscription.id),
limit: Some(100),
..Default::default()
}).await?;
println!("{} signals", fired.count);
```
### PATCH: omitting a field vs clearing it
On the update params, nullable fields are `Option<Option<T>>` so the two cases
stay distinguishable on the wire:
| Rust | JSON | Effect |
|---|---|---|
| `None` | key omitted | leave the stored value untouched |
| `Some(None)` | `"name": null` | clear the stored value |
| `Some(Some(v))` | `"name": "v"` | set it to `v` |
```rust
client.price_alerts.update(alert_id, &PriceAlertUpdateParams {
name: Some(None), // clear the label
is_active: Some(false), // pause it
..Default::default() // everything else untouched
}).await?;
```
Price alerts only accept `name`, `delivery_mode`, `webhook_url` and `is_active`
on PATCH β `token_address` / `drop_pct` / `recovery_pct` are immutable, because
retuning a threshold mid-flight would make the alert's recorded events
uninterpretable. First-touch `filters` is a whole-object **replace**, not a
merge, so "remove this filter" stays expressible.
## Streaming
Six WebSocket channels carry Robinhood Chain events live (same
`wss://madeonsol.com/ws/v1/stream` protocol as the Solana stream client):
| Channel constant | Value | Tier | Payload |
|---|---|---|---|
| `stream::RHC_KOL_TRADES` | `rhc:kol_trades` | PRO+ (connection gate) | Every tracked-KOL buy/sell on chain 4663 (`rhc:kol_trade` events) |
| `stream::RHC_DEX_TRADES` | `rhc:dex_trades` | **ULTRA+** | The full RHC DEX swap firehose, ~40-55 trades/s at tip (`rhc:dex_trade` events) |
| `stream::RHC_COPYTRADE_SIGNALS` | `rhc:copytrade:signals` | PRO+ | Your copy-trade rule fires (`rhc:copytrade:signal`, user-scoped) |
| `stream::RHC_PRICE_ALERT_EVENTS` | `rhc:price_alert:events` | PRO+ | Your price-alert fires (`rhc:price_alert:dip` / `rhc:price_alert:recovery`, user-scoped, ~15s polled) |
| `stream::RHC_KOL_COORDINATION` | `rhc:kol:coordination` | PRO+ | Your coordination-alert rule fires (`rhc:kol:coordination`, user-scoped) |
| `stream::RHC_KOL_FIRST_TOUCHES` | `rhc:kol:first_touches` | PRO+ | Every token's FIRST tracked-KOL buy (`rhc:kol:first_touch`, **broadcast** β ULTRA gates only the first-touch subscription CRUD, not this channel) |
> **Fixed in 0.5.0:** 0.4.0's `stream::RHC_TRADES` pointed at `rhc:trades`, a
> channel that never existed server-side β subscribing drew a
> `channels_rejected` warning and then silence β and its docs claimed PRO+
> where the real firehose gate is ULTRA+. The constant now carries the real
> name `rhc:dex_trades` (and is deprecated in favor of `RHC_DEX_TRADES`); the
> server additionally accepts `rhc:trades` as a deprecated alias for 0.4.0
> clients. The four rule-engine channel constants above are new in 0.5.0.
The server never fails a `subscribe` outright: channels your tier cannot
access are dropped and reported in a warning frame
(`{"type":"warning","code":"channels_rejected","rejected":[{"channel":"...","reason":"requires ULTRA"}],"valid_channels":[...],"ts":...}`)
followed by the normal `subscribed` ack. Watch for it β a rejected channel
otherwise looks like a healthy but silent subscription.
```rust
let ws = client.stream.get_token().await?; // POST /stream/token (PRO+)
// Connect to `ws.ws_url` with `?token=<ws.token>` appended, then subscribe to
// robinhood_chain::api::stream::RHC_KOL_TRADES / RHC_DEX_TRADES / the four
// rule-engine channel constants.
```
**Stream tokens do not expire** *(since 2026-08-27)*. `get_token()` returns the
same token on every call β call it on every reconnect and never schedule a
refresh: `expires_at` / `next_refresh_at` are always `None` (kept for wire
compatibility only). The token stops working only when your subscription
lapses, or when you replace it yourself with `client.stream.rotate_token()`
(`POST /stream/token` with `{"rotate": true}`) β the old value then keeps
working for 60 s so live sockets can reconnect. A WebSocket close code `4001`
means "call `get_token()` again and reconnect", never "the token timed out".
```rust
// Only if a token leaked β there is no reason to rotate on a schedule.
let fresh = client.stream.rotate_token().await?;
assert_eq!(fresh.rotated, Some(true));
```
## Error handling
Every call returns `Result<T, robinhood_chain::Error>`. API errors carry the HTTP
status and the parsed body:
```rust
match client.tokens.get("0xnot_a_token").await {
Ok(token) => println!("{:?}", token.symbol),
Err(e) => {
if let Some(404) = e.status() { println!("token not on Robinhood Chain"); }
else { eprintln!("{e}"); }
}
}
```
## Links
- Robinhood Chain overview β <https://madeonsol.com/robinhood>
- Pricing & tiers β <https://madeonsol.com/pricing>
- Full API reference β <https://madeonsol.com/api-docs>
- Get a free API key β <https://madeonsol.com/pricing>
## License
MIT Β© MadeOnSol