Build Robinhood Chain price alerts with the WebSocket stream
Build Robinhood Chain price alerts on the Crius WebSocket price channel, with a runnable Node.js alert bot that checks liquidity and token security first.
Subscribe to price: plus the token address on wss://api.crius.sh/v1/robinhood/stream. You get a snapshot right away, then updates with usd, eth and liquidity_usd whenever the token trades (at most 10 a second). Compare usd with your thresholds, ignore updates with too little liquidity_usd, and check GET /v1/robinhood/token/{address}/security once before you alert. Free plan: 10 tokens per connection.
A price alert on Robinhood Chain is a rule (“tell me when AI crosses $0.10”, “tell me when TSLA moves 3% in 15 minutes”) checked against a live price. The Crius WebSocket gives you that price on the price:<token> channel: a snapshot as soon as you subscribe, then a new usd, eth and liquidity_usd every time the token trades. A good alert bot adds two checks the price alone can’t give it: enough liquidity behind the price, and a token that isn’t a trap.
What the price channel sends
Subscribe with {"op": "subscribe", "channels": ["price:0x2e8c31162b855a2ffa90f6f8634643ad6f111e18"]}. Right after the subscribed reply, the server sends the current price with "snapshot": true and no seq; after that, each update is a normal stream message.
| Field | Meaning |
|---|---|
data.usd |
Price per whole token in USD |
data.eth |
Price per whole token in ETH |
data.liquidity_usd |
Liquidity of the pool the price came from (2 × its quote side) |
block, ts |
The block and chain time (unix ms) of the trade that moved the price |
snapshot |
true on the first message after subscribing |
Updates are capped at 10 a second per token, and only come when the token trades. The stream reference covers the envelope, heartbeats and reorgs.
Check the price source before you rely on it
The REST price has the same usd, eth and liquidity_usd plus the pool, a confidence level and a thin-liquidity flag. Call it once when a user creates an alert, to decide whether the token is worth alerting on at all.
Artificial Inu's price and where it comes from
A user wants an alert on Artificial Inu (AI, 0x2e8c31162b855a2ffa90f6f8634643ad6f111e18). Is its price solid enough to alert on?
curl "https://api.crius.sh/v1/robinhood/token/0x2e8c31162b855a2ffa90f6f8634643ad6f111e18/price"const res = await fetch("https://api.crius.sh/v1/robinhood/token/0x2e8c31162b855a2ffa90f6f8634643ad6f111e18/price");const { data, meta } = await res.json();import requests
r = requests.get( "https://api.crius.sh/v1/robinhood/token/0x2e8c31162b855a2ffa90f6f8634643ad6f111e18/price",)body = r.json()print(body["data"])Real response 200 Captured Oct 28, 2026 · 25 lines (arrays trimmed)
{ "data": { "confidence": "high", "dex": "uniswap_v4", "eth": 0.0000341958, "eth_usd": 2355.49, "liquidity_usd": 657440, "quote": "0x39dbed3a2bd333467115de45665cc57f813c4571", "quote_liquidity_usd": 328720, "source_pool": "0x230bc4a0fb5fb7ab27f6e72350dfa3f2489b0cdbaf34f0189d68672086ccb919", "symbol": "AI", "thin_liquidity": false, "token": "0x2e8c31162b855a2ffa90f6f8634643ad6f111e18", "usd": 0.0805479 }, "meta": { "block": 100410151, "block_age_ms": 315, "block_ts": 1793225421000, "chain": "robinhood", "finalized_block": 100401329, "lag_ms": 26, "safe_block": 100405028 }}What this tells you
usd: 0.0805479comes from a Uniswap v4 pool (dex: "uniswap_v4",source_poolis its 32-byte pool id), quoted in another token rather than ETH (quoteis0x39dbed3a2bd333467115de45665cc57f813c4571).liquidity_usd: 657440is 2 × the real quote side (quote_liquidity_usd: 328720). That’s the same number the stream sends inliquidity_usd, so a floor such as $10,000 works on both.confidence: "high"andthin_liquidity: false: a single small trade can’t move this price far. For a token showingthin_liquidity: true, require a larger move or a second update before alerting.
The same token's security, checked once per alert
Before I tell users AI is pumping, is anything wrong with the token?
curl "https://api.crius.sh/v1/robinhood/token/0x2e8c31162b855a2ffa90f6f8634643ad6f111e18/security"const res = await fetch("https://api.crius.sh/v1/robinhood/token/0x2e8c31162b855a2ffa90f6f8634643ad6f111e18/security");const { data, meta } = await res.json();import requests
r = requests.get( "https://api.crius.sh/v1/robinhood/token/0x2e8c31162b855a2ffa90f6f8634643ad6f111e18/security",)body = r.json()print(body["data"])Real response 200 Captured Oct 28, 2026 · 127 lines (arrays trimmed)
{ "data": { "contract": { "blacklist": false, "can_change_limits": false, "can_change_tax": false, "cooldown": false, "implementation": null, "is_proxy": false, "mintable": false, "owner": "0xeb7c034704ef8dcd2d32324c1545f62fb4ad0862", "ownership": "active", "pausable": false, "proxy_kind": null, "self_destruct": false, "trading_switch": false, "upgradeable": false, "verified": "none" }, "flags": [ "ownership_active", "unverified" ], "honeypot": { "is_honeypot": false, "reason": null, "selective": false }, "hook": null, "known_asset": false, "launch": { "flags": [], "lp_top_wallet": { "address": "0x9701fb0ade1e269c8f64ec0c7b3cfadb31a13a52", "liquidity_usd": 703900, "pct": 19.8021 }, "sniper_supply_pct": 1.70207, "sniper_top_wallet_pct": 1.70207, "sniper_wallets": 1, "sniper_window_blocks": 10, "uniform_holders": null, "wallet_farm": null }, "limits": {}, "liquidity": { "status": "partially_locked", "summary": { "burned_pct": 14.4982, "locked_pct": 0, "next_unlock_ts": 1794049200000, "protocol_fee_pct": 0, "unlocked_in_wallets_pct": 73.5823, "weighting": "by pool liquidity_usd" } }, "primary_dex": "uniswap_v4", "risk": "low", "score": 85, "tax": { "buy_pct": 0, "confidence": "high", "realized": { "buy_pct": 0, "by_wallet_class": { "dev_sell_pct": null, "others_sell_pct": 0 }, "last_sample_ts": 1793225667000, "max_sell_pct_seen": 99.99, "samples": { "buys": 98750, "sells": 101250 }, "sell_pct": 0, "window": "30d" }, "sell_pct": 0, "simulated": { "amount_eth": 0.042421762259500888, "buy_gas": 145730, "buy_pct": 0, "can_buy": true, "can_sell": true, "can_transfer": true, "from": "eoa", "revert_reason": null, "router": "0x89e5db8b5aa49aa85ac63f691524311aeb649eba", "sell_gas": 126783, "sell_pct": 0, "transfer_pct": 0 }, "source": "realized", "transfer_pct": 0 }, "tax_history": [ { "from_block": 99878573, "pct": 0, "side": "buy" }, { "from_block": 99878935, "pct": 100, "side": "buy" }, { "from_block": 99880233, "pct": 0, "side": "buy" } ], "tax_history_total": 919, "tax_mode": "dynamic", "token": "0x2e8c31162b855a2ffa90f6f8634643ad6f111e18" }, "meta": { "block": 100412468, "block_age_ms": 741, "block_ts": 1793225673000, "chain": "robinhood", "finalized_block": 100401329, "lag_ms": 5, "safe_block": 100405028, "simulation": "eth_simulateV1 via Uniswap V2 router at head" }}What this tells you
score: 85,risk: "low", withownership_active(the owner can still act on the contract) andunverifiedas the only flags.- No taxes:
tax.buy_pctandtax.sell_pctare 0, measured over 98,750 buys and 101,250 sells (tax.realized.samples), and the simulated buy, transfer and sell all went through (tax.simulated.can_sell: true, via the Uniswap V2 router named inmeta.simulation). liquidity.status: "partially_locked": about 73.6% of the LP sits unlocked in wallets (unlocked_in_wallets_pct) andnext_unlock_tsis 1794049200000 (November 7, 2026). A sharp drop could be liquidity leaving, so an alert bot can also subscribe tolp:for the token.
An alert bot in Node.js
The bot reads rules, checks each token once, subscribes to their price channels and sends a message when a rule triggers. Each rule fires once, then re-arms after the price moves back past it. Delivery here is a webhook ({"content": ...} works with Discord webhooks); swap notify for email, Telegram or a push service.
// alerts.mjs: CRIUS_API_KEY=... WEBHOOK_URL=... node alerts.mjs (Node.js 22+)const KEY = process.env.CRIUS_API_KEY;const API = "https://api.crius.sh/v1/robinhood";const MIN_LIQUIDITY_USD = 10_000;
const RULES = [ { token: "0x2e8c31162b855a2ffa90f6f8634643ad6f111e18", name: "AI", above: 0.1 }, { token: "0x2e8c31162b855a2ffa90f6f8634643ad6f111e18", name: "AI", below: 0.07 }, { token: "0x322f0929c4625ed5bad873c95208d54e1c003b2d", name: "TSLA", movePct: 3, windowMs: 15 * 60_000 },];
const blocked = new Set(); // tokens that failed the security checkconst history = {}; // token -> [{ ts, usd }] for move ruleslet backoff = 1000;
async function notify(text) { console.log(text); if (process.env.WEBHOOK_URL) { await fetch(process.env.WEBHOOK_URL, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ content: text }) }); }}
async function screen(token) { const res = await fetch(`${API}/token/${token}/security`, { headers: { "x-api-key": KEY } }); const { data } = await res.json(); if (!data || data.risk === "critical" || data.honeypot?.is_honeypot) { blocked.add(token); console.warn(`not alerting on ${token}: ${data?.risk} ${data?.flags?.join(", ")}`); }}
function evaluate(token, p, ts) { if (blocked.has(token) || p.liquidity_usd < MIN_LIQUIDITY_USD) return; const h = (history[token] ??= []); h.push({ ts, usd: p.usd }); while (h.length && h[0].ts < ts - 60 * 60_000) h.shift(); // keep one hour
for (const r of RULES.filter((r) => r.token === token)) { let hit = false; let why = ""; if (r.above != null) [hit, why] = [p.usd >= r.above, `above $${r.above}`]; if (r.below != null) [hit, why] = [p.usd <= r.below, `below $${r.below}`]; if (r.movePct != null) { const base = h.find((x) => x.ts >= ts - r.windowMs)?.usd; const pct = base ? ((p.usd - base) / base) * 100 : 0; [hit, why] = [Math.abs(pct) >= r.movePct, `${pct.toFixed(2)}% in ${r.windowMs / 60_000} min`]; } if (hit && !r.fired) notify(`${r.name} $${p.usd} (${why}), liquidity $${Math.round(p.liquidity_usd)}`).catch(console.error); r.fired = hit; // re-arm once the condition is no longer true }}
function connect() { const tokens = [...new Set(RULES.map((r) => r.token))].filter((t) => !blocked.has(t)); const ws = new WebSocket(`wss://api.crius.sh/v1/robinhood/stream?api_key=${KEY}`); let idle; const touch = () => { clearTimeout(idle); idle = setTimeout(() => ws.close(), 45_000); }; ws.onopen = () => { backoff = 1000; touch(); ws.send(JSON.stringify({ op: "subscribe", channels: tokens.map((t) => `price:${t}`) })); }; ws.onmessage = (e) => { touch(); const m = JSON.parse(e.data); if (m.ch === "error") return console.error(m.data); if (!m.ch.startsWith("price:") || m.data.removed) return; evaluate(m.ch.slice(6), m.data, m.ts ?? Date.now()); // the snapshot has no ts: use the clock }; ws.onclose = () => { clearTimeout(idle); setTimeout(connect, backoff + Math.random() * 500); backoff = Math.min(backoff * 2, 30_000); }; ws.onerror = () => ws.close();}
for (const t of new Set(RULES.map((r) => r.token))) await screen(t);connect();-
Screen once, not per update. One security call per token (1 credit, or free without a key at 2 requests per second) when the alert is created. Re-check daily, or subscribe to
tax:andlp:for the token to hear about tax changes and liquidity removal. -
Use the snapshot. After every reconnect the snapshot gives the current price immediately. A price alert doesn’t need
resume: the latest price is what counts, and the snapshot is it. -
Floor the liquidity. Skip updates whose
liquidity_usdis under your floor. Meme tokens often have one deep pool and several thin ones; the stream’s price comes from the deepest, but a token whose only pool is thin is easy to push across a threshold. -
Mind your subscription count. The
hellomessage carriesmax_subscriptions. Past it, the server answerschannel_deniedwith the channels it refused; split them across connections (Starter and up) or poll batch prices for the rest.
Frequently asked questions
How often does the price channel update?
Every time the token trades, at most once per 100 ms per token. A token that doesn't trade sends nothing, so a quiet price is not a stale connection: the 15-second heartbeat tells you the socket is alive.
How many tokens can one alert bot watch?
One price: subscription per token. The Free plan allows 10 subscriptions on 1 connection, Starter 100 on each of 3 connections, Pro 1,000 on each of 10. For a long watch list on a small plan, poll batch prices (100 tokens per 1-credit call) and stream only the tokens closest to a threshold.
Why did my alert fire on a price nobody can trade at?
A thin pool can print a price far from the real market. Each price message carries liquidity_usd (2 × the quote side of the pool the price came from): skip updates below a floor you choose. The REST price also returns confidence and thin_liquidity for a second look before you send.
Do price alerts need an API key?
The stream does: a Free key with the stream scope. The REST calls the bot makes (price, security, batch prices) also work without a key on the anonymous plan, at 2 requests per second.
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