CRIUS
Trading botsWebSocketTutorial

Building a Robinhood Chain trading bot with WebSocket streams

Architecture and working code for a Robinhood Chain trading bot: stream new launches and trades, filter with security and holder checks, track prices in real time and handle reconnects and reorgs.

Crius 4 min read

A trading bot lives or dies on two things: seeing events early and filtering out the bad ones before it acts. This guide builds the data side of a Robinhood Chain bot with the Crius API: a WebSocket stream for events, REST calls for checks, and the reconnect and reorg handling that keeps it honest. Execution (signing and sending swaps) is up to you; we only cover the data.

Architecture

WebSocket stream ──► event queue ──► filters (REST checks) ──► strategy ──► your executor
launches:all /security
graduations:all /bundles
price:<token> /dev/{address}
trades:<token>
  • Stream for anything time-sensitive: new launches, graduations, prices of tokens you hold.
  • REST for one-off checks on a candidate token: tax, snipers, the creator’s history.
  • A queue between them, so a slow check never blocks the stream.

You need a key with the stream scope. The Free plan includes one stream connection with 10 subscriptions; Starter gives 3 connections and 100 subscriptions.

Step 1: listen for candidates

const KEY = process.env.CRIUS_API_KEY;
const queue = [];
const held = new Set(); // tokens we hold, watched by price
function connect() {
const ws = new WebSocket(`wss://api.crius.sh/v1/robinhood/stream?api_key=${KEY}`);
ws.onopen = () =>
ws.send(JSON.stringify({
op: "subscribe",
channels: ["graduations:all", ...[...held].map((t) => `price:${t}`)],
}));
ws.onmessage = (e) => {
const m = JSON.parse(e.data);
if (m.data?.removed) return onRemoved(m); // reorged: see step 4
if (m.ch.startsWith("graduations:")) queue.push({ token: m.data.token, pool: m.data.pool, block: m.block });
if (m.ch.startsWith("price:")) onPrice(m.ch.slice(6), m.data.usd);
};
ws.onclose = () => setTimeout(connect, 2000);
}
connect();

This bot watches graduations rather than raw launches: a token that completed its bonding curve has real liquidity in a Uniswap pool, which filters out most of the noise. Swap in launches:all for an earlier, riskier strategy.

Step 2: filter with REST checks

Each candidate gets a few quick checks before the strategy sees it:

const api = (path) =>
fetch(`https://api.crius.sh/v1/robinhood${path}`, { headers: { "x-api-key": KEY } }).then((r) => r.json());
async function vet(token) {
const [sec, bundles, profile] = await Promise.all([
api(`/token/${token}/security`),
api(`/token/${token}/bundles`),
api(`/token/${token}`),
]);
const reasons = [];
if (sec.data.honeypot.is_honeypot) reasons.push(`honeypot: ${sec.data.honeypot.reason}`);
if ((sec.data.tax.sell_pct ?? 0) > 10) reasons.push(`sell tax ${sec.data.tax.sell_pct}%`);
if (sec.data.risk === "high") reasons.push("high risk");
if (profile.data.holders.top10_pct > 60) reasons.push("top 10 hold over 60%");
if (profile.data.market.quote_liquidity_usd < 5000) reasons.push("under $5k of real liquidity");
return { ok: reasons.length === 0, reasons, sec, bundles };
}
setInterval(async () => {
const c = queue.shift();
if (!c) return;
const v = await vet(c.token);
if (v.ok) strategy(c, v);
else console.log("skip", c.token, v.reasons.join(", "));
}, 500);

Good filters to consider:

  • Tax: /security reports buy and sell tax measured from real trades, plus a simulated sell. Skip anything you can’t sell, or that taxes sells heavily.
  • Real liquidity: use quote_liquidity_usd (the ETH or USDG you could actually get out), not the 2× liquidity_usd.
  • Snipers and bundles: /bundles shows how much supply the first-block buyers and coordinated wallets hold. They’re often the first to sell.
  • The creator: /dev/{address} shows their past tokens. A creator whose last twenty tokens all went to zero is a signal.

Step 3: track what you hold

When the strategy buys, add the token to held and subscribe to price:<token>. Price updates arrive as the pool moves, so stop-losses and take-profits react within a block or two, without polling:

function onPrice(token, usd) {
const pos = positions.get(token);
if (!pos) return;
if (usd <= pos.entry * 0.7 || usd >= pos.entry * 2) exit(token, usd);
}

To see the flow behind a move, subscribe to trades:<token> as well: every trade comes with the attributed trader, side and USD value.

Step 4: reconnects and reorgs

Two things that separate a toy bot from a reliable one:

  • Reconnect with resume. Keep the last seq per channel, and after reconnecting send {"op":"resume","seqs":{…}}. The server replays up to 60 seconds of missed messages. The stream example has a complete client with backoff and idle detection.
  • Undo reorged events. If blocks are replaced, you get a reorg message and then every affected event again with "removed": true and the same id. If a graduation you queued gets removed, drop it. If you need certainty before acting (larger positions), subscribe with "finality": "safe": events then arrive once their batch is on Ethereum, minutes later instead of milliseconds.

Budget

Streams cost 1 credit per connection, not per message, and most REST checks cost 1–10 credits. A bot vetting a few hundred candidates a day fits comfortably in the Free plan’s 1,000,000 monthly credits; plans and limits has the exact costs.

Nothing in this article is trading advice: test against small amounts, and remember that every filter is a heuristic.

Build with Crius

Robinhood Chain data in one API

Free plan with 1M credits a month. Your key in under a minute.

Related APIs

More from the blog