Build a Robinhood Chain token screener
Build a Robinhood Chain token screener with the Crius API: clean token lists, batch profiles, a risk check per token, credits per refresh and working code.
Poll GET /v1/robinhood/tokens for the lists you show (top_volume, new, graduating, trending…) with filters such as clean=true and min_liquidity_usd, add holder and lock data with POST /v1/robinhood/tokens/batch (100 tokens per call), and check each new token once with /security. Every call costs 1 credit and works without a key at 2 requests per second.
A token screener is a table of Robinhood Chain tokens that users filter and sort: by volume, price change, liquidity, market cap, launchpad or risk. With the Crius API it is three calls: token lists for the rows and their trading stats, batch profiles for fields the list doesn’t carry (holder concentration, liquidity locks), and security once per token for a risk badge. All three are on the anonymous plan and cost 1 credit per call with a key.
Architecture
-
Poller. A server job calls
/tokensevery 30 seconds for each list you show, with your base filters (clean=true,min_liquidity_usd,launchpad,window). Each call returns up to 100 rows with price, market cap, liquidity, holders and per-window stats. -
Enricher. For the tokens in those lists, one
POST /tokens/batchper 100 tokens addsholders.top10_pct,holders.dev_pct,liquidity.statusandliquidity.locked_pct. -
Risk cache. Tokens you haven’t seen in the last hour get a
/securitycall; storerisk,scoreandflags. -
API for your UI. Users’ filters and sorts run on that in-memory table, so a page view costs no credits. Optionally, a stream subscription to
launches:alladds new tokens the moment they launch.
Endpoints used: GET/tokens POST/tokens/batch GET/token/{address}/security GET/prices
The rows: a token list with filters
Top 24h volume, clean tokens with $10k+ liquidity
Which Robinhood Chain tokens traded the most in the last 24 hours, leaving out rugs, wash trading and thin markets?
curl "https://api.crius.sh/v1/robinhood/tokens?list=top_volume&window=24h&clean=true&min_liquidity_usd=10000&limit=3"const res = await fetch("https://api.crius.sh/v1/robinhood/tokens?list=top_volume&window=24h&clean=true&min_liquidity_usd=10000&limit=3");const { data, meta } = await res.json();import requests
r = requests.get( "https://api.crius.sh/v1/robinhood/tokens?list=top_volume&window=24h&clean=true&min_liquidity_usd=10000&limit=3",)body = r.json()print(body["data"])Real response 200 Captured Oct 28, 2026 · 281 lines (arrays trimmed)
{ "data": [ { "address": "0xd0601ce157db5bdc3162bbac2a2c8af5320d9eec", "buy_volume_usd": { "1h": 284727, "24h": 5466713, "5m": 4788.48, "6h": 2762630 }, "buyers": { "1h": 237, "24h": 1993, "5m": 25, "6h": 786 }, "buys": { "1h": 837, "24h": 16502, "5m": 48, "6h": 6856 }, "change_pct": { "1h": 0.222526, "24h": -1.4982, "5m": -0.129097, "6h": -1.39644 }, "created_block": 153563, "created_ts": 1782222069300, "decimals": 18, "fdv_usd": 18950323, "holders": 192282, "launchpad": null, "launchpad_name": null, "links": { "discord": null, "farcaster": null, "github": null, "telegram": null, "twitter": null, "website": null }, "liquidity_usd": 5692665, "logo": null, "market_cap_usd": 18950201, "name": "NVIDIA • Robinhood Token", "price_eth": 0.0909845, "price_usd": 214.313, "progress_pct": null, "quote": "USDG", "sell_volume_usd": { "1h": 203391, "24h": 6094025, "5m": 54662.2, "6h": 3389490 }, "sellers": { "1h": 230, "24h": 2643, "5m": 31, "6h": 1098 }, "sells": { "1h": 724, "24h": 18645, "5m": 107, "6h": 8674 }, "status": null, "symbol": "NVDA", "traders": { "1h": 329, "24h": 3513, "5m": 45, "6h": 1417 }, "txns": { "1h": 1453, "24h": 33049, "5m": 145, "6h": 14479 }, "volume_usd": { "1h": 488118, "24h": 11560738, "5m": 59450.7, "6h": 6152120 } }, { "address": "0x117cc2133c37b721f49de2a7a74833232b3b4c0c", "buy_volume_usd": { "1h": 280776, "24h": 5360063, "5m": 8943.58, "6h": 3449730 }, "buyers": { "1h": 173, "24h": 1786, "5m": 24, "6h": 920 }, "buys": { "1h": 596, "24h": 10514, "5m": 26, "6h": 5156 }, "change_pct": { "1h": 0.329135, "24h": 0.110872, "5m": 0.350832, "6h": 0.71074 }, "created_block": 153733, "created_ts": 1782222864140, "decimals": 18, "fdv_usd": 20226775, "holders": 70703, "launchpad": null, "launchpad_name": null, "links": { "discord": null, "farcaster": null, "github": null, "telegram": null, "twitter": null, "website": null }, "liquidity_usd": 16569917, "logo": null, "market_cap_usd": 20226751, "name": "SPDR S&P 500 ETF Trust • Robinhood Token", "price_eth": 0.328203, "price_usd": 773.079, "progress_pct": null, "quote": "USDG", "sell_volume_usd": { "1h": 342684, "24h": 4970409, "5m": 74111.8, "6h": 3375154 }, "sellers": { "1h": 218, "24h": 1924, "5m": 39, "6h": 971 }, "sells": { "1h": 881, "24h": 11039, "5m": 85, "6h": 5766 }, "status": null, "symbol": "SPY", "traders": { "1h": 329, "24h": 2757, "5m": 54, "6h": 1416 }, "txns": { "1h": 1396, "24h": 20497, "5m": 103, "6h": 10312 }, "volume_usd": { "1h": 623461, "24h": 10330472, "5m": 83055.3, "6h": 6824884 } }, { "address": "0x4a0e65a3eccec6dbe60ae065f2e7bb85fae35eea", "buy_volume_usd": { "1h": 133427, "24h": 4487530, "5m": 801.518, "6h": 1670779 }, "buyers": { "1h": 118, "24h": 1173, "5m": 7, "6h": 499 }, "buys": { "1h": 452, "24h": 14559, "5m": 9, "6h": 5347 }, "change_pct": { "1h": -0.00703424, "24h": -3.40455, "5m": -0.0708291, "6h": -0.22877 }, "created_block": 412861, "created_ts": 1782740860100, "decimals": 18, "fdv_usd": 13549210, "holders": 116370, "launchpad": null, "launchpad_name": null, "links": { "discord": null, "farcaster": null, "github": null, "telegram": null, "twitter": null, "website": null }, "liquidity_usd": 3716808, "logo": null, "market_cap_usd": 13549000, "name": "Space Exploration Technologies Corp. Class A Common Stock • Robinhood Token", "price_eth": 0.0611357, "price_usd": 144.005, "progress_pct": null, "quote": "USDG", "sell_volume_usd": { "1h": 138415, "24h": 5276839, "5m": 11840.2, "6h": 1775402 }, "sellers": { "1h": 145, "24h": 1584, "5m": 15, "6h": 693 }, "sells": { "1h": 499, "24h": 15226, "5m": 25, "6h": 5620 }, "status": null, "symbol": "SPCX", "traders": { "1h": 180, "24h": 1956, "5m": 21, "6h": 821 }, "txns": { "1h": 887, "24h": 28427, "5m": 34, "6h": 10519 }, "volume_usd": { "1h": 271842, "24h": 9764370, "5m": 12641.7, "6h": 3446181 } } ], "meta": { "block": 100409896, "block_age_ms": 750, "block_ts": 1793225395000, "chain": "robinhood", "finalized_block": 100401329, "lag_ms": 160, "list": "top_volume", "next_cursor": "3", "safe_block": 100405028, "stats_since": "chain_start", "window": "24h" }}What this tells you
- Tokenized stocks lead: NVIDIA (
0xd0601ce157db5bdc3162bbac2a2c8af5320d9eec,volume_usd["24h"]11,560,738), the S&P 500 ETF (0x117cc2133c37b721f49de2a7a74833232b3b4c0c, 10,330,472) and SpaceX Class A (0x4a0e65a3eccec6dbe60ae065f2e7bb85fae35eea, 9,764,370), all quoted in USDG (quote: "USDG"). A meme-token screener addslaunchpad=pons(or another launchpad id) to the same call. - Each row has stats for
5m,1h,6hand24h: NVIDIA’s 24h has 33,049txnsfrom 3,513traders, with moresellers(2,643) thanbuyers(1,993) andchange_pctof −1.5%. holders(192,282 for NVIDIA),liquidity_usd,market_cap_usdandfdv_usdare on the row, so most screener columns need no second call.launchpad,statusandprogress_pctarenullfor tokens not launched on a launchpad; for curve tokens they give the launchpad id,bonding/graduatedand curve progress.meta.next_cursorpages to the next rows;limitgoes up to 100.
The extra columns: batch profiles
Five fields for two tokens in one call
I have a list of addresses. How do I get just the fields my table needs?
curl -X POST "https://api.crius.sh/v1/robinhood/tokens/batch" \ -H "content-type: application/json" \ -d '{"tokens":["0x7b8892cea949bc60d494681d1bc3ea96071ab0b8","0x0bd7d308f8e1639fab988df18a8011f41eacad73"],"fields":["symbol","price.usd","price.change_pct.24h","market.market_cap_usd","market.liquidity_usd"]}'const res = await fetch("https://api.crius.sh/v1/robinhood/tokens/batch", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ "tokens": [ "0x7b8892cea949bc60d494681d1bc3ea96071ab0b8", "0x0bd7d308f8e1639fab988df18a8011f41eacad73" ], "fields": [ "symbol", "price.usd", "price.change_pct.24h", "market.market_cap_usd", "market.liquidity_usd" ] }),});const { data, meta } = await res.json();import requests
r = requests.post( "https://api.crius.sh/v1/robinhood/tokens/batch", json={"tokens":["0x7b8892cea949bc60d494681d1bc3ea96071ab0b8","0x0bd7d308f8e1639fab988df18a8011f41eacad73"],"fields":["symbol","price.usd","price.change_pct.24h","market.market_cap_usd","market.liquidity_usd"]},)body = r.json()print(body["data"])Real response 200 Captured Oct 28, 2026 · 40 lines (arrays trimmed)
{ "data": { "0x0bd7d308f8e1639fab988df18a8011f41eacad73": { "market": { "liquidity_usd": 73135530, "market_cap_usd": 90409474 }, "price": { "change_pct": { "24h": 0.286017 }, "usd": 2355.49 }, "symbol": "WETH" }, "0x7b8892cea949bc60d494681d1bc3ea96071ab0b8": { "market": { "liquidity_usd": 5611.8, "market_cap_usd": 3773.42 }, "price": { "change_pct": { "24h": -0.171971 }, "usd": 0.00000388948 }, "symbol": "CHARGE" } }, "meta": { "block": 100406780, "block_age_ms": 575, "block_ts": 1793225076000, "chain": "robinhood", "finalized_block": 100397649, "lag_ms": 196, "missing": [], "safe_block": 100401329 }}What this tells you
fieldstakes dot paths into the token profile; the response has exactly those paths per token, keyed by address.- WETH (
0x0bd7d308f8e1639fab988df18a8011f41eacad73) hasmarket.liquidity_usdof 73,135,530; CHARGE (0x7b8892cea949bc60d494681d1bc3ea96071ab0b8) has 5,611.8 and a market cap of 3,773.42. - Unknown addresses go to
meta.missinginstead of failing the call (empty here), so one bad row never breaks a refresh.
Use it in your app
This server keeps the table in memory, refreshes it every 30 seconds and answers GET /screener?min_liq=50000&max_top10=30&sort=volume for your front end.
// screener.mjs: node screener.mjs, then GET http://localhost:8080/screener?sort=change (Node.js 22+)import { createServer } from "node:http";
const API = "https://api.crius.sh/v1/robinhood";const HEADERS = { "content-type": "application/json", ...(process.env.CRIUS_API_KEY && { "x-api-key": process.env.CRIUS_API_KEY }) };const LISTS = ["list=top_volume&window=24h&clean=true&min_liquidity_usd=10000", "list=new&min_liquidity_usd=5000", "list=graduating"];const FIELDS = ["holders.top10_pct", "holders.dev_pct", "liquidity.status", "liquidity.locked_pct"];
const rows = new Map(); // address -> rowconst risk = new Map(); // address -> { risk, score, flags, at }
const call = async (path, body) => { const res = await fetch(`${API}${path}`, body ? { method: "POST", headers: HEADERS, body: JSON.stringify(body) } : { headers: HEADERS }); return (await res.json()).data;};
async function refresh() { for (const q of LISTS) for (const t of (await call(`/tokens?${q}&limit=100`)) ?? []) rows.set(t.address, { ...rows.get(t.address), ...t, seen: Date.now() }); for (const [a, r] of rows) if (Date.now() - r.seen > 10 * 60_000) rows.delete(a); // drop rows no list returned for 10 min
const addrs = [...rows.keys()]; for (let i = 0; i < addrs.length; i += 100) { const extra = (await call("/tokens/batch", { tokens: addrs.slice(i, i + 100), fields: FIELDS })) ?? {}; for (const [a, p] of Object.entries(extra)) if (rows.has(a)) Object.assign(rows.get(a), { top10_pct: p.holders?.top10_pct, dev_pct: p.holders?.dev_pct, lp: p.liquidity?.status, locked_pct: p.liquidity?.locked_pct }); }
for (const a of addrs.filter((a) => Date.now() - (risk.get(a)?.at ?? 0) > 3_600_000).slice(0, 50)) { const s = await call(`/token/${a}/security`); if (s) risk.set(a, { risk: s.risk, score: s.score, flags: s.flags, at: Date.now() }); }}
const SORTS = { volume: (r) => r.volume_usd?.["24h"] ?? 0, change: (r) => r.change_pct?.["24h"] ?? -Infinity, liquidity: (r) => r.liquidity_usd ?? 0, mcap: (r) => r.market_cap_usd ?? 0 };
createServer((req, res) => { const q = new URL(req.url, "http://x").searchParams; const out = [...rows.values()] .map((r) => ({ ...r, ...risk.get(r.address) })) .filter((r) => (r.liquidity_usd ?? 0) >= Number(q.get("min_liq") ?? 0)) .filter((r) => r.top10_pct == null || r.top10_pct <= Number(q.get("max_top10") ?? 100)) .filter((r) => r.risk !== "critical" && (q.get("risk") !== "low" || r.risk === "low")) .sort((a, b) => (SORTS[q.get("sort")] ?? SORTS.volume)(b) - (SORTS[q.get("sort")] ?? SORTS.volume)(a)) .slice(0, 100); res.writeHead(200, { "content-type": "application/json" }); res.end(JSON.stringify(out));}).listen(8080);
const loop = () => refresh().catch(console.error).finally(() => setTimeout(loop, 30_000));loop();What it costs
| Operation | Endpoint | Credits |
|---|---|---|
| One list page (up to 100 tokens) | GET /tokens |
1 |
| Extra fields for up to 100 tokens | POST /tokens/batch |
1 |
| Risk for one token | GET /token/{address}/security |
1 |
| Prices only, up to 100 tokens | GET /prices |
1 |
With three lists refreshed every 30 seconds, the lists cost 8,640 credits a day and the batch calls about the same again (up to 300 tokens, three calls per refresh). Re-checking the risk of up to 300 rows once an hour adds at most 7,200 a day. That’s about 740,000 credits a month, inside the Free plan’s 1,000,000; refreshing every 60 seconds halves the list and batch part. Without a key the same calls run on the anonymous plan at 2 requests per second, which is enough for a prototype, not for a refresh of this size. See Plans & limits.
Frequently asked questions
How do I keep rugs and wash trading out of a screener?
Add clean=true to any list. It applies the trending list's checks: no crash of 90% or more from the window's peak, $1,000+ liquidity, 20+ real holders, no insider or wallet-farm launch, no bot distribution, and no single wallet holding 40%+ of the liquidity unlocked. Then show each token's /security risk next to it.
How many tokens can I fetch per call?
Up to 100 per page from /tokens (paginate with cursor), and up to 100 per call from /tokens/batch and /prices. Each of those calls costs 1 credit regardless of how many tokens it returns.
Can a screener update in real time?
Lists are a poll: refreshing every 15–60 seconds is enough for most screeners. For live rows, subscribe to token:<address> on the WebSocket stream (price and holder count changes) for the tokens on screen, within your plan's subscription limit.
Which windows do the list stats cover?
List rows carry volume_usd, buy_volume_usd, sell_volume_usd, txns, buys, sells, buyers, sellers, traders and change_pct for 5m, 1h, 6h and 24h. window (for example window=1h) chooses which one ranks the list.
Related
Get your free Crius API key
Every example on this page works with a free key: higher rate limits than the anonymous plan, holders, wallets and the WebSocket stream. No card needed.
- Robinhood Chain (4663) only, from our own archive nodes
- REST + WebSocket
- Pay on-chain or by card