robinhood-chain
⭐ Star on GitHub · 📂 Examples · 📚 API docs · 🏦 Robinhood Chain hub
Robinhood Chain SDK for Python — EVM-native trading intelligence, chain id 4663.
Live KOL trades and consensus clustering, token discovery, launch-bundle detection, early-buyer quality, holders and live on-chain risk, deployer reputation with alerts and trajectory, the Uniswap v2/v3/v4 trade tape, OHLC candles, batch token lookups, smart-money wallet ranking, and four push rule engines (copy-trade, price alerts, KOL coordination, first touches) — for Robinhood Chain (an Arbitrum Orbit L2, chain id 4663), served from our self-hosted node. Everything is EVM-native: lowercase 0x addresses (token_address), 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 — the same msk_ API key and the same base URL. Free tier: 200 requests/day, no card. Get a key at madeonsol.com/pricing.
New in 0.9.1 — stream tokens never expire.
POST /api/v1/stream/tokennow returns the same token on every call, forever (server change of 2026-08-27).expires_at/next_refresh_atare alwaysNonenow and kept only for wire compatibility; the response gainedrotated: boolandlifetime: str. A token only stops working when the subscription lapses or you replace it with the newclient.stream_token(rotate=True)(the previous value keeps working for 60 s). The server never rotates on its own and never sendstoken_refreshunless you rotated; a4001close means "mint again", never a timer. Preferred handshake auth isAuthorization: Bearer <token>(?token=still works and is masked in access logs); RHC channels ride the same socket and token as Solana.client.stream()already fetched a token on every (re)connect and never readexpires_at, so its behavior is unchanged — only its docs are.
New in 0.9.0 — tokenized equities + the rug signal. Two endpoints that were live on the API but had no Python binding:
client.equities(sort=, limit=, symbol=, q=)→GET /rhc/equities(BASIC,EquitiesResponse/EquityTypedDicts) 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.lp_events(limit=, token=, pool=, provider=, dex=, before=)→GET /rhc/lp-events(PRO+,LpEventsResponse/LpEvent) is the liquidity removals feed — Uniswap v2/v3Burn+ v4ModifyLiquiditywith a negative delta on tracked pools, each row enriched with the token, the provider wallet,provider_is_token_deployer(the classic rug tell) andprovider_kol_name. Removals ONLY: adds are not persisted, so an empty page means "no removals seen", never "no liquidity activity" — thecoverageblock saysadds_persisted: False. Amounts are raw uint256 strings; v4 rows carryliquidityonly. Cursor vianext_before; the path alias/rhc/tokens/{address}/lp-eventsis the same feed withtoken=pinned. Data since 2026-08-05. Both are key-mode only (not on the keyless x402 rail).
New in 0.8.0 —
holder_growth: who arrived and who left.client.token_holders(address)(HolderGrowth/HolderGrowthWindowTypedDicts) now returnsholder_growthonGET /rhc/tokens/{address}/holders:{ "1h", "24h", "7d" }×{ cutoff_block, entered, entered_still_holding, exited, net }. entered = addresses whose firstTransferof 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 inholder_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 isnull(never 0) only when the chain had no ingested trades in it; the whole block isnullonly if the growth read failed. Sanity check from ship day: a token launched that morning showed 593 entered / 560 still holding over 24h, andholder_countwas exactly 560.
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:
wallet(),wallet_pnl(),wallet_positions(),wallet_trades(), plus the watchlist —wallet_tracker_list(),wallet_tracker_add(),wallet_tracker_remove(),wallet_tracker_relabel(),wallet_tracker_trades()andwallet_tracker_summary(). Everything is ETH-denominated, and cost basis is FIFO over a rolling 90-day window —cost_basis_observable_fromnames 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_hitsays which call paid for it. Watchlist quotas are per chain (PRO 50 / ULTRA 100 / BUSINESS 500 RHC wallets), independent of your Solana list.
New in 0.7.0 — keyless x402 mode.
RobinhoodClient(private_key="0x…"): any EVM wallet holding USDG on Robinhood Chain can call the 10 keyless endpoints (kol_feed,kol_hot_tokens,kol_leaderboard,token,token_buyer_quality,token_kol_consensus,token_risk,token_holders,wallet_pnl,deployer_alerts) with no API key — the client handles the 402 → sign EIP-3009transferWithAuthorization(EIP-712 domain{Global Dollar, 1, 4663}) → retry flow, one payment per call, from $0.04. The wallet needs USDG but no ETH (our facilitator relays gas).client.last_paymentcarries the on-chain settlement (transaction,payer). Needs the extrapip install "robinhood-chain[x402]"(eth-account). Any other method on a keyless client raisesKeylessNotAvailableError— it names the rail, it never silently downgrades. Sync + async both supported.
New in 0.5.0 — real-time WebSocket streaming. A managed stream client (
client.stream()) overwss://madeonsol.com/ws/v1/streamwith auto-reconnect, token handling and typed callbacks, covering all six RHC channels — the KOL tape, the full DEX firehose, and the four rule-engine push channels. Channel names are the canonical server registry (rhc:dex_trades, not therhc:tradesspelling some 0.4.0 SDKs used — the server still accepts that as a deprecated alias). Needs thestreamextra:pip install "robinhood-chain[stream]". See Real-time streaming.
Quick start (10 seconds)
pip install robinhood-chain
from robinhood_chain import RobinhoodClient
client = RobinhoodClient(api_key="msk_...") # free key at https://madeonsol.com/pricing
# Real-time KOL trades on Robinhood Chain (chain id 4663)
feed = client.kol_feed(limit=5, action="buy")
for t in feed["trades"]:
print(t["kol_name"], t["action"], t["token_address"], t["eth_amount"], "ETH", t["tx_hash"])
GET https://madeonsol.com/api/v1/rhc/kol/feed → every buy/sell from tracked 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), sub-second from execution, enriched with live MC and mc_multiple_since_trade ("did the call run").
Authentication
Two modes. Key mode — Bearer msk_ API key, the same key and base URL as the Solana MadeOnSol API, all 54 operations. Keyless x402 mode (0.7.0) — private_key= of an EVM wallet holding USDG on Robinhood Chain pays per call on the 10-endpoint rail documented at madeonsol.com/robinhood/x402; needs pip install "robinhood-chain[x402]".
import os
from robinhood_chain import RobinhoodClient
# Keyless: USDG wallet on chain 4663, no signup. Read the key from the environment.
agent = RobinhoodClient(private_key=os.environ["RHC_PAYER_KEY"])
risk = agent.token_risk("0xd0601ce157db5bdc3162bbac2a2c8af5320d9eec") # NVDA, $0.02
print(risk["score"], agent.last_payment["transaction"]) # settlement tx on Robinhood Chain
import os
from robinhood_chain import RobinhoodClient
client = RobinhoodClient(api_key=os.environ["MADEONSOL_API_KEY"])
Endpoints — the 54 Robinhood Chain operations
Base URL https://madeonsol.com/api/v1. All addresses are lowercase 0x (40 hex). Everything is a GET except the two batch POSTs and the four rule engines, which are full CRUD.
KOL intelligence
| Method | Route | Tier |
|---|---|---|
client.kol_feed(limit=, before=, action=, kol=, min_eth=) |
GET /api/v1/rhc/kol/feed |
BASIC |
client.kol_leaderboard(period=, limit=) |
GET /api/v1/rhc/kol/leaderboard |
BASIC |
client.kol_hot_tokens(window=) |
GET /api/v1/rhc/kol/hot-tokens |
BASIC |
client.kol_coordination(period=, min_kols=, limit=, min_mc_usd=, max_mc_usd=) |
GET /api/v1/rhc/kol/coordination |
BASIC |
client.kol_first_touches(limit=, since=, before=, min_eth=, token_age_max_min=, launchpad=, min_mc_usd=, max_mc_usd=) |
GET /api/v1/rhc/kol/first-touches |
BASIC |
client.kol_wallet(wallet) |
GET /api/v1/rhc/kol/{wallet} |
BASIC |
Trades & tokens
| Method | Route | Tier |
|---|---|---|
client.trades(limit=, token=, dex=, action=, min_eth=, before=) |
GET /api/v1/rhc/trades |
PRO+ |
client.lp_events(limit=, token=, pool=, provider=, dex=, before=) — liquidity removals only (v2/v3 Burn + v4 negative ModifyLiquidity; coverage["adds_persisted"] is False), raw uint256 string amounts, provider_is_token_deployer = rug tell |
GET /api/v1/rhc/lp-events |
PRO+ |
client.tokens(limit=, sort=, min_mc_usd=, min_liquidity_usd=, launchpad=) |
GET /api/v1/rhc/tokens |
PRO+ |
client.equities(sort=, limit=, symbol=, q=) — every official Robinhood tokenized stock/ETF; identity = issuer beacon, never the name; live price / MC / liquidity + 24h trades / ETH volume / buyers vs sellers; sort volume|trades|market_cap|last_trade|symbol, limit ≤ 300 |
GET /api/v1/rhc/equities |
BASIC |
client.token(address) |
GET /api/v1/rhc/tokens/{address} |
BASIC |
client.token_batch(addresses) — max 50 |
POST /api/v1/rhc/token/batch |
BASIC |
client.token_candles(address, limit=, from_=, to=) |
GET /api/v1/rhc/tokens/{address}/candles |
PRO+ |
client.token_kol_consensus(address) |
GET /api/v1/rhc/tokens/{address}/kol-consensus |
PRO+ |
client.token_buyer_quality(address) |
GET /api/v1/rhc/tokens/{address}/buyer-quality |
BASIC |
client.tokens_batch_buyer_quality(addresses) — max 20 |
POST /api/v1/rhc/tokens/batch/buyer-quality |
BASIC |
client.token_bundle(address) |
GET /api/v1/rhc/tokens/{address}/bundle |
BASIC |
client.token_top_traders(address, limit=, offset=) |
GET /api/v1/rhc/tokens/{address}/top-traders |
PRO+ |
client.token_flow(address, window=) |
GET /api/v1/rhc/tokens/{address}/flow |
PRO+ |
client.token_peak_history(address, window=, curve=) |
GET /api/v1/rhc/tokens/{address}/peak-history |
PRO+ |
client.token_risk(address) |
GET /api/v1/rhc/tokens/{address}/risk |
PRO+ |
client.token_holders(address, limit=, offset=) — exact holders + concentration from Transfer logs (check verified), plus holder_growth ("1h" / "24h" / "7d": entered, entered_still_holding, exited, net ≈ Δ holder_count; pools/burns excluded, a window is None only when the chain had no ingested trades in it) |
GET /api/v1/rhc/tokens/{address}/holders |
PRO+ |
Deployer hunter
| Method | Route | Tier |
|---|---|---|
client.deployer_hunter_leaderboard(sort=, tier=, min_tokens=, limit=, offset=) |
GET /api/v1/rhc/deployer-hunter/leaderboard |
BASIC |
client.deployer_hunter_alerts(deployer_tier=, priority=, alert_type=, launchpad=, min_mc=, include_untradeable=, since=, before=, limit=, offset=) |
GET /api/v1/rhc/deployer-hunter/alerts |
BASIC |
client.deployer_hunter_best_tokens(period=, limit=) |
GET /api/v1/rhc/deployer-hunter/best-tokens |
BASIC |
client.deployer_hunter_recent_bonds(deployer_tier=, min_peak=, limit=) |
GET /api/v1/rhc/deployer-hunter/recent-bonds |
BASIC |
client.deployer_hunter_stats() |
GET /api/v1/rhc/deployer-hunter/stats |
BASIC |
client.deployer_hunter_profile(address) |
GET /api/v1/rhc/deployer-hunter/{address} |
BASIC |
client.deployer_hunter_trajectory(address) |
GET /api/v1/rhc/deployer-hunter/{address}/trajectory |
BASIC |
client.deployer_hunter_tokens(address, limit=, offset=, sort=) |
GET /api/v1/rhc/deployer-hunter/{address}/tokens |
BASIC |
client.deployer_hunter_history(address, limit=, offset=) |
GET /api/v1/rhc/deployer-hunter/{address}/history |
PRO+ |
Alpha wallets
| Method | Route | Tier |
|---|---|---|
client.alpha_wallets(classification=, identity=, min_memecoin_share=, sort=, limit=, offset=, ...) |
GET /api/v1/rhc/alpha-wallets |
PRO+ |
Rule engines — push, not polling
Four server-side rule engines that watch the RHC tape for you and deliver over webhook or WebSocket. Every quota is per chain — configuring RHC rules never consumes your Solana budget. webhook_secret is returned exactly once on create; payloads are signed HMAC-SHA256 over <timestamp>.<body> in the X-MadeOnSol-Signature header.
| Method | Route | Tier |
|---|---|---|
client.copytrade_subscriptions_list() |
GET /api/v1/rhc/copytrade/subscriptions |
PRO+ |
client.copytrade_subscriptions_create(source_wallets=, sizing_amount=, name=, min_trade_eth=, only_action=, sizing_mode=, delivery_mode=, webhook_url=) |
POST /api/v1/rhc/copytrade/subscriptions |
PRO+ |
client.copytrade_subscriptions_get(subscription_id) |
GET /api/v1/rhc/copytrade/subscriptions/{id} |
PRO+ |
client.copytrade_subscriptions_update(subscription_id, **fields) |
PATCH /api/v1/rhc/copytrade/subscriptions/{id} |
PRO+ |
client.copytrade_subscriptions_delete(subscription_id) |
DELETE /api/v1/rhc/copytrade/subscriptions/{id} |
PRO+ |
client.copytrade_signals(subscription_id=, since=, limit=) |
GET /api/v1/rhc/copytrade/signals |
PRO+ |
client.price_alerts_list() |
GET /api/v1/rhc/price-alerts |
PRO+ |
client.price_alerts_create(token_address=, drop_pct=, name=, recovery_pct=, delivery_mode=, webhook_url=) |
POST /api/v1/rhc/price-alerts |
PRO+ |
client.price_alerts_get(alert_id) |
GET /api/v1/rhc/price-alerts/{id} |
PRO+ |
client.price_alerts_update(alert_id, **fields) |
PATCH /api/v1/rhc/price-alerts/{id} |
PRO+ |
client.price_alerts_delete(alert_id) |
DELETE /api/v1/rhc/price-alerts/{id} |
PRO+ |
client.price_alerts_events(alert_id=, event_type=, since=, limit=) |
GET /api/v1/rhc/price-alerts/events |
PRO+ |
client.coordination_alerts_list() |
GET /api/v1/rhc/kol/coordination/alerts |
PRO+ |
client.coordination_alerts_create(min_kols=, window_minutes=, min_score=, cooldown_min=, score_jump_break=, min_mc_usd=, max_mc_usd=, delivery_mode=, webhook_url=) |
POST /api/v1/rhc/kol/coordination/alerts |
PRO+ |
client.coordination_alerts_get(rule_id) |
GET /api/v1/rhc/kol/coordination/alerts/{id} |
PRO+ |
client.coordination_alerts_update(rule_id, **fields) |
PATCH /api/v1/rhc/kol/coordination/alerts/{id} |
PRO+ |
client.coordination_alerts_delete(rule_id) |
DELETE /api/v1/rhc/kol/coordination/alerts/{id} |
PRO+ |
client.first_touch_subscriptions_list() |
GET /api/v1/rhc/kol/first-touches/subscriptions |
ULTRA+ |
client.first_touch_subscriptions_create(name=, filters=, delivery_mode=, webhook_url=) |
POST /api/v1/rhc/kol/first-touches/subscriptions |
ULTRA+ |
client.first_touch_subscriptions_get(subscription_id) |
GET /api/v1/rhc/kol/first-touches/subscriptions/{id} |
ULTRA+ |
client.first_touch_subscriptions_update(subscription_id, **fields) |
PATCH /api/v1/rhc/kol/first-touches/subscriptions/{id} |
ULTRA+ |
client.first_touch_subscriptions_delete(subscription_id) |
DELETE /api/v1/rhc/kol/first-touches/subscriptions/{id} |
ULTRA+ |
Copy-trade rules are ETH-denominated and carry no MC band — the RHC notify payload has 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, or a filter that silently never matches.
RHC price alerts are polled (~15s), not live. rhc_token_prices is written by the RHC ingester on a separate box and emits no pg_notify, so there is nothing to react to. Effective latency is that interval plus the token's own price-update cadence — do not assume parity with the Solana alerts, which are sub-second. The create response says so in its evaluation block. token_address, drop_pct and recovery_pct are immutable once set; delete and recreate to retune.
Coordination scoring is comparable to Solana, not identical. The shared v1 scorer runs and quality is a real KOL win-rate, but earliness is defaulted — RHC has no early-entry equivalent. Every fired signal records which components were real in score_inputs.
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. On update, filters is a whole-object replace, not a merge.
Clearing a field: NULL vs omitting it
Omitting a keyword leaves the field untouched; passing NULL sets it to JSON null. Python's None cannot mean both, and the routes validate with strict schemas that reject an explicit null on non-nullable fields. Only name, webhook_url, min_mc_usd and max_mc_usd are nullable on the wire.
from robinhood_chain import RobinhoodClient, NULL
client = RobinhoodClient(api_key="msk_...")
# Follow three wallets, 0.05 ETH per copy, pushed over WebSocket
sub = client.copytrade_subscriptions_create(
name="degen desk",
source_wallets=["0xaaa...", "0xbbb...", "0xccc..."],
min_trade_eth=0.01,
sizing_mode="fixed",
sizing_amount=0.05,
delivery_mode="websocket",
)
# Catch up on anything the webhook missed
sigs = client.copytrade_signals(subscription_id=sub["subscription"]["id"], limit=100)
# Pause the rule and drop its label — is_active is untouched by the NULL
client.copytrade_subscriptions_update(sub["subscription"]["id"], name=NULL, is_active=False)
# Alert me if this token drops 30% from where it is right now
client.price_alerts_create(
token_address="0xToken...", drop_pct=30, recovery_pct=15,
webhook_url="https://example.com/hook",
)
Deployer tiers — what elite actually means
elite / good are earned on the $100K runner_rate and require 24h of deployer history (migrations 267 + 269). The $40K bar proved farmable by operators mass-relaunching one ticker across rotating wallets, so graduation_rate — which still means "share of launches that reached a $40K+ peak MC", and is still returned everywhere — no longer determines the tier. spammer is the one exception and still keys off graduation_rate, because detecting trash is a different question from detecting quality.
client.deployer_hunter_stats() returns the live tier_rules, graduation_definition ($40K) and runner_definition ($100K), so you never have to guess what a label currently means.
Examples
KOL leaderboard & consensus
# KOLs ranked by trade count then net ETH flow (net_eth = buy − sell, not PnL)
lb = client.kol_leaderboard(period="24h", limit=20) # '24h' | '7d' | '30d'
for row in lb["leaderboard"]:
print(row["kol_name"], row["trades"], "trades", row["net_eth"], "ETH net")
# Tokens bought by 2+ distinct KOLs in the window (consensus signal)
hot = client.kol_hot_tokens(window="1h") # '5m'|'15m'|'1h'|'6h'|'24h'
for tok in hot["tokens"]:
print(tok["token_symbol"], tok["kols_buying"], "KOLs", tok["buy_eth"], "ETH")
# One KOL's profile: last-200-trade stats + 50 most recent trades
me = client.kol_wallet("0x1234567890abcdef1234567890abcdef12345678")
print(me["stats"]["net_eth"], me["stats"]["tokens_traded"])
KOL coordination & first touches
# Coordination — the cohort BEHIND a hot token: who bought, who already exited,
# how fast they piled in. signal is 'accumulating' or 'distributing'.
co = client.kol_coordination(period="6h", min_kols=3, limit=10, max_mc_usd=250_000)
for tok in co["coordination"]:
print(tok["token_symbol"], tok["kol_count"], "KOLs",
tok["signal"], tok["net_eth"], "ETH net",
tok["holders_count"], "holding /", tok["exited_count"], "exited",
tok["time_to_consensus_sec"], "s to consensus")
for k in tok["kols"]:
print(" ", k["name"], k["buy_eth"], "ETH in", "EXITED" if k["exited"] else "holding")
# First touches — the FIRST time any tracked KOL bought a token (discovery signal).
# token_age_max_min isolates genuinely early calls; poll forward with `since`.
ft = client.kol_first_touches(limit=25, token_age_max_min=30, min_eth=0.05)
for e in ft["events"]:
print(e["token_symbol"], e["first_kol"]["name"],
e["eth_amount"], "ETH at", e["market_cap_usd_at_first_buy"], "MC",
"→ peak", e["peak_mc_usd"], e["tx_hash"])
# BASIC clamps limit to 20; first_kol['evm_address'] is ULTRA/BUSINESS only.
# Page back with ft["next_before"], or poll forward with since=<newest first_buy_at>.
Token bundle + early-buyer quality
addr = "0xabcdef1234567890abcdef1234567890abcdef12"
# Launch-bundle detection — RHC is an Arbitrum Orbit L2, so bundle_kind is
# 'same_block' or 'none' (there is NO atomic_tx on EVM).
b = client.token_bundle(addr)
print(b["bundle"]["bundle_kind"], b["bundle"]["held_ratio"], b["bundle"]["fully_exited"])
# 0–100 early-buyer quality — win-rate, KOL-presence, bundle + dump-cluster legs
q = client.token_buyer_quality(addr)
print(q["quality"]["score"], q["quality"]["signal"], q["quality"]["breakdown"])
# KOL consensus (PRO+): net_flow_eth, exit rate, median entry MC; ULTRA adds wallet lists
c = client.token_kol_consensus(addr)
if c["consensus"]:
print(c["consensus"]["net_flow_eth"], c["consensus"]["kol_exit_rate"])
Batch lookups
watchlist = ["0xaaa...", "0xbbb...", "0xccc..."]
# Up to 50 tokens in ONE call — metadata, live price/MC/FDV/liquidity, peak MC,
# and the deployer reputation block. Set-based server-side, not a fan-out.
batch = client.token_batch(watchlist)
for tok in batch["tokens"]:
if not tok["found"]: # every REQUESTED address is echoed back
print(tok["address"], "not indexed on Robinhood Chain")
continue
print(tok["symbol"], tok["market_cap_usd"], tok["liquidity_usd"], tok["peak_mc_usd"])
# Early-buyer quality for up to 20 tokens. The cap is 20, NOT the Solana 50:
# RHC buyer-quality is a per-token cohort computation, not one set-based query.
# A token that fails to score comes back as an entry with an "error" key rather
# than failing the whole batch.
bq = client.tokens_batch_buyer_quality(watchlist[:20])
for r in bq["tokens"]:
if "error" in r:
print(r["token_address"], "score failed:", r["error"])
else:
print(r["token_address"], r["quality"]["score"], r["quality"]["signal"])
print(bq["scored"], "of", bq["requested"], "scored; cap is", bq["max_addresses"])
DEX trade tape & candles (PRO+)
# Every Uniswap v2/v3/v4 swap — trader_eoa is the effective trading account
# (tx.from, or the ERC-4337 userOp sender when bundled), never the router or the bundler
tape = client.trades(dex="uniswap-v3", min_eth=0.1, limit=50)
for s in tape["trades"]:
print(s["trader_eoa"], s["action"], s["eth_amount"], "ETH", s["tx_hash"], s["block_number"])
# 1-minute OHLC candles (oldest → newest)
candles = client.token_candles(addr, limit=240)
for k in candles["candles"]:
print(k["bucket_start"], k["close_price_usd"], k["volume_usd"])
Tokenized equities & liquidity removals
# Every beacon-verified Robinhood stock/ETF token, ranked by 24h ETH volume —
# identity is the issuer beacon, never the name, so the fake NVDA/GameStop contracts never show up
eq = client.equities(sort="volume", limit=20)
print(eq["identity"]["method"], eq["total_equities"])
for e in eq["equities"]:
print(e["symbol"], e["name"], e["price_usd"], "MC", e["market_cap_usd"], e["trades_24h"], "trades", e["volume_eth_24h"], "ETH")
nvda = client.equities(symbol="NVDA") # exact ticker, case-insensitive
# Rug watch — liquidity REMOVALS for one token (PRO+). Adds are never persisted:
# coverage["adds_persisted"] is False, so an empty page means "no removals seen".
lp = client.lp_events(token=addr, limit=50)
for ev in lp["events"]:
if ev["provider_is_token_deployer"]:
print("deployer pulled LP:", ev["tx_hash"], ev["dex"], ev["token_amount_raw"]) # raw uint256 string
Deployer reputation & smart money
# Deployer reputation leaderboard — graduation_rate = share reaching $40K+ peak MC,
# runner_rate = share reaching $100K+ (most RHC launchpads are direct-to-DEX).
# The elite/good tier rides runner_rate + 24h of deployer history; graduation_rate
# is still returned but no longer sets the tier (only `spammer` still uses it).
lb = client.deployer_hunter_leaderboard(sort="runner_rate", tier="elite", min_tokens=3, limit=20)
for d in lb["deployers"]:
print(d["deployer_address"], d["tier"], d["graduation_rate"], d["runner_rate"])
# One deployer — unknown wallets return is_deployer: false (not a 404)
prof = client.deployer_hunter_profile("0x1111111111111111111111111111111111111111")
print(prof["is_deployer"], prof.get("recent_tokens_count"))
# Smart-money wallet ranking — net_eth is realized net flow (sell − buy)
sm = client.alpha_wallets(classification="smart_money", min_memecoin_share=0.7, sort="net_eth", limit=25)
for w in sm["wallets"]:
print(w["wallet"], w["classification"], w["net_eth"], "ETH", w["win_rate"])
Deployer alerts, stats & the chain-wide picture
# Live deployer signal feed. alert_type is 'new_deploy' | 'graduated',
# priority is 'high' | 'medium' (RHC has no bonded/kol_buy/low).
alerts = client.deployer_hunter_alerts(deployer_tier="elite", alert_type="new_deploy", limit=50)
print(alerts["tradability_filter"]) # e.g. 'liquidity_usd >= $100'
for a in alerts["alerts"]:
print(a["token_symbol"], a["tier"], a["mc_at_alert"], "MC",
a["liquidity_usd"], "liq", a["event_at"])
if a["tier_is_stale"]:
print(" was", a["tier_at_alert"], "when the alert fired, now", a["tier"])
# Poll forward: pass the newest event_at back as `since` to get only what's new.
new = client.deployer_hunter_alerts(since=alerts["next_event_at"])
Two behaviour changes worth knowing about this feed (2026-07-25):
- A tradability filter is ON by default. Alerts whose token has
liquidity_usdunder $100 — or unknown liquidity, which on RHC usually means a drained pool — are dropped, because a $45K-MC alert on a token with $68 of liquidity is not a signal. Passinclude_untradeable=Truefor the raw tape (archive/leaderboard tooling); the active setting is echoed astradability_filter. tieris resolved at read time. It is the deployer's current tier, not the snapshot taken when the alert fired — that snapshot comes back astier_at_alert, withtier_is_staleset when the two disagree.deployer_tier=filters on the resolved value, andmessageis restated in terms of the $100K runner rate that now sets the tier.
# Chain-wide reputation summary — the denominator for "is this deployer rare?"
st = client.deployer_hunter_stats()
print(st["total_deployers"], "deployers,", st["reputable_deployers"], "reputable")
print(st["by_tier"], st["spam_token_share"], st["alerts_24h"], "alerts/24h")
print(st["tier_rules"]) # the ACTIVE thresholds — elite/good ride runner_rate
print(st["graduation_definition"]) # 'peak market cap >= $40,000'
print(st["runner_definition"]) # 'peak market cap >= $100,000'
# Best tokens from deployers worth tracking (elite/good only, ranked by peak MC)
best = client.deployer_hunter_best_tokens(period="7d", limit=10)
for tokn in best["tokens"]:
print(tokn["symbol"], tokn["peak_mc_usd"], "peak", tokn["deployer"]["tier"])
if best.get("truncated"):
print("top-N drawn from the 1000 most recent launches, not the whole period")
# Recent graduations — the $40K peak-MC milestone (NOT a bonding curve; RHC
# launchpads are direct-to-DEX). min_peak only raises that floor.
bonds = client.deployer_hunter_recent_bonds(deployer_tier="good", min_peak=75_000, limit=25)
for tokn in bonds["tokens"]:
print(tokn["symbol"], tokn["peak_mc_usd"], tokn["peak_mc_at"], tokn["deployer_tier"])
One deployer, in depth
dep = "0x1111111111111111111111111111111111111111"
# Getting better or worse? Streaks, rolling 10-launch success rate, best/worst
# stretches, deploy cadence, and a trend of 'improving' | 'declining' | 'stable'.
# Success here is the $40K graduation milestone (echoed as success_metric) — NOT
# the $100K runner bar that sets tiers, because $100K is too rare to form a curve.
tj = client.deployer_hunter_trajectory(dep)
if tj["is_deployer"]:
t = tj["trajectory"]
print(tj["success_metric"], t["trend"], t["current_streak"],
"longest hit streak", t["longest_bond_streak"],
"avg", t["avg_days_between_deploys"], "days between deploys")
# Full paginated launch history with live + peak MC.
# ⚠️ sort='peak_mc_usd' sorts the REQUESTED PAGE only (sort_scope: 'page') —
# use deployer_hunter_best_tokens() for a real cross-deployer ranking.
page = client.deployer_hunter_tokens(dep, limit=100, offset=0, sort="first_seen_at")
for tokn in page["tokens"]:
print(tokn["symbol"], tokn["first_seen_at"], tokn["peak_mc_usd"], tokn["liquidity_usd"])
print(page["total"], "total,", "more pages" if page["has_more"] else "end")
# PRO+: the same history with graduation detail and an exact total.
hist = client.deployer_hunter_history(dep, limit=500)
for tokn in hist["tokens"]:
print(tokn["symbol"], tokn["is_graduated"], tokn["graduated_at"], tokn["graduated_pool"])
Async
Every endpoint has an async twin via client.aclient() — same signature, returns a coroutine:
import asyncio
from robinhood_chain import RobinhoodClient
client = RobinhoodClient(api_key="msk_...")
async def main():
a = client.aclient()
feed = await a.kol_feed(limit=10)
print(feed["count"])
# Batch POSTs have async twins too
batch = await a.token_batch(["0xaaa...", "0xbbb..."])
print(batch["found"], "of", batch["requested"], "found")
# So do the rule-engine writes (POST / PATCH / DELETE)
rules = await a.coordination_alerts_list()
print(len(rules["rules"]), "coordination rules")
asyncio.run(main())
Real-time streaming (new in 0.5.0)
Managed WebSocket stream — auto-reconnect with backoff, token fetch on every (re)connect (POST /api/v1/stream/token under the hood — stream tokens never expire since 2026-08-27, so there is no refresh timer), heartbeat liveness, and typed callbacks. Needs the stream extra:
pip install "robinhood-chain[stream]"
import asyncio
from robinhood_chain import RobinhoodClient
client = RobinhoodClient(api_key="msk_...")
async def main():
stream = client.stream()
@stream.on("rhc:kol_trade")
async def on_kol_trade(data, evt):
print(data["kol_name"], data["action"], data["token_address"], data["eth_amount"], "ETH")
@stream.on("rhc:kol:first_touch")
async def on_first_touch(data):
print("FIRST TOUCH", data["token_address"])
@stream.on("warning")
async def on_warning(msg):
# e.g. code == "channels_rejected": you asked for a channel that does
# not exist or that your tier cannot hold — msg["rejected"] carries a
# per-channel reason, msg["valid_channels"] the full list.
print("stream warning:", msg)
stream.subscribe(["rhc:kol_trades", "rhc:kol:first_touches"])
await stream.run() # blocks; manages connection + reconnects
asyncio.run(main())
All six RHC channels ride the main stream endpoint (wss://madeonsol.com/ws/v1/stream). Unlike Solana, the RHC DEX firehose has no separate endpoint — it is the rhc:dex_trades channel here. The stream token itself is PRO+.
| Channel | What it delivers (event names) | Tier |
|---|---|---|
rhc:kol_trades |
Every tracked-KOL trade on chain 4663 (rhc:kol_trade) |
PRO+ |
rhc:dex_trades |
The full DEX firehose — every attributed Uniswap v2/v3/v4 swap, ~40–55/s at tip (rhc:dex_trade) |
ULTRA+ |
rhc:copytrade:signals |
Your copy-trade rule fires, user-scoped (rhc:copytrade:signal) |
PRO+ |
rhc:price_alert:events |
Your price-alert dips/recoveries, user-scoped (rhc:price_alert:dip / rhc:price_alert:recovery) — ~15s polled server-side, not sub-second |
PRO+ |
rhc:kol:coordination |
Coordination-alert fires (rhc:kol:coordination) |
PRO+ |
rhc:kol:first_touches |
Broadcast first-touch feed (rhc:kol:first_touch) — the channel is PRO+; ULTRA gates the first-touch subscription CRUD endpoints, not this broadcast |
PRO+ |
Lifecycle events: open, close, reconnect, subscribed, heartbeat, warning, error, plus "*" for every data event. Deprecated spelling: the server accepts rhc:trades as an alias of rhc:dex_trades (some 0.4.0 SDKs shipped it); this SDK uses only canonical names.
Invalid channels are never silent. If a subscribe names an unknown or tier-gated channel, the server answers with a {type: "warning", code: "channels_rejected", rejected, valid_channels} frame. The stream client delivers it to your on("warning") handler — and if you registered none, surfaces it via Python's warnings.warn so a rejected channel can't masquerade as a quiet market.
If you'd rather hand-roll the WebSocket, client.stream_token() (sync) / client.aclient().stream_token() (async) returns {"token", "expires_at", "next_refresh_at", "rotated", "lifetime", "ws_url", "channels", ...} — connect to {ws_url} with an Authorization: Bearer {token} header (?token={token} still works and is masked in access logs) and send {"type": "subscribe", "channels": [...]}. The token never expires: expires_at / next_refresh_at are always None, the same token comes back on every call, and client.stream_token(rotate=True) is the only way to replace it (the old one keeps working for 60 s). A 4001 close means "mint again", never a timer.
Errors & rate limits
Non-2xx responses raise a typed error carrying the API's error message and _rid request id:
from robinhood_chain import RobinhoodClient, AuthError, TierError, NotFoundError, RateLimitError
client = RobinhoodClient(api_key="msk_...")
try:
client.trades(limit=50) # PRO+
except TierError as e:
print("upgrade needed:", e.message, e.request_id)
except RateLimitError as e:
print("slow down; resets at", e.reset)
except NotFoundError as e:
print("no RHC data:", e.message)
# Rate-limit headers from the most recent call:
print(client.last_rate_limit)
# {'limit': 100, 'remaining': 92, 'used': 8, 'reset': 1714000000, 'request_id': 'rid_abc'}
AuthError (401), TierError (403), NotFoundError (404), RateLimitError (429) all subclass RobinhoodAPIError → RobinhoodError. Transient failures (429/5xx) are retried automatically with exponential backoff (max_retries, default 2).
Tiers
| Tier | Robinhood Chain endpoints |
|---|---|
| BASIC (free) | KOL feed/leaderboard/hot-tokens/coordination/first-touches/profile, token snapshot + batch, buyer-quality (single + batch), bundle, deployer-hunter leaderboard/alerts/best-tokens/recent-bonds/stats/profile/trajectory/tokens |
| PRO+ | + DEX trade tape, token discovery, candles, KOL consensus, top-traders, flow, peak-history, risk, holders, deployer-hunter history, alpha-wallets, and the copy-trade / price-alert / coordination rule engines |
| ULTRA | + full alert pagination (limit above 50), KOL evm_address on first-touches, full bundle cohort + consensus wallet lists, first-touch push subscriptions |
Robinhood Chain is bundled into every tier at no extra cost. Get a key at madeonsol.com/pricing.
Also available for Robinhood Chain
| Platform | Package |
|---|---|
| TypeScript SDK | robinhood-chain-sdk (npm) |
| Rust SDK | robinhood-chain (crates.io) |
| MCP server | mcp-server-robinhood-chain (npm) |
Links
- Robinhood Chain hub — https://madeonsol.com/robinhood
- Pricing & free key — https://madeonsol.com/pricing
- API docs — https://madeonsol.com/api-docs
License
MIT
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file robinhood_chain-0.9.1.tar.gz.
File metadata
- Download URL: robinhood_chain-0.9.1.tar.gz
- Upload date:
- Size: 79.3 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/7.0.0 CPython/3.12.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
b7c38c184cb195ec64168e8237aa112d9d039b0fefe1c866cefd3d40c65282ba
|
|
| MD5 |
b63f90a8f39d2ea634e7e79eeadbc7e2
|
|
| BLAKE2b-256 |
2cba1105a946deaed60aa63cc6af7f2864e7f5f66dcc16b686020f29df2e4094
|
File details
Details for the file robinhood_chain-0.9.1-py3-none-any.whl.
File metadata
- Download URL: robinhood_chain-0.9.1-py3-none-any.whl
- Upload date:
- Size: 55.8 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/7.0.0 CPython/3.12.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
dfef3df6403cd332e8ca53dabddbc02d919a9f483c9c2514531d51878aa4f023
|
|
| MD5 |
99273d6f36177e7e4e840a4bc86a2b5e
|
|
| BLAKE2b-256 |
25f15a4abd143a78461ff4d0fb7d6fa32b5019f6cd21cc05afe72e5f3f2a308d
|