Stream live candles for a Robinhood Chain token chart
Keep a Robinhood Chain candlestick chart live: load candles over REST, update the last bar from the Crius WebSocket, with a runnable Node.js relay.
Load history with GET /v1/robinhood/token/{address}/candles?interval=1m, then subscribe to candles: plus the token address and interval (for example :1m) on wss://api.crius.sh/v1/robinhood/stream. Each trade pushes the current candle (t, o, h, l, c, v, n): replace your last bar when t matches and append a new one when it's later. Intervals 1s to 1d; Free plan.
A live chart needs two things: the history, and the candle that is still forming. Crius gives you the history from the REST candles endpoint and the forming candle from the WebSocket channel candles:<token>:<interval>, which pushes the current bucket’s open, high, low, close, USD volume and trade count after every trade. Your chart replaces its last bar when the bucket start t is the same and appends a bar when it moves on. The stream needs a Free key with the stream scope, so it runs on your server and relays candles to the page.
Load the history first
TSLA one-minute candles, with a gap
What do the last minutes of Tesla's tokenized stock (0x322f0929c4625ed5bad873c95208d54e1c003b2d) look like on a 1-minute chart?
curl "https://api.crius.sh/v1/robinhood/token/0x322f0929c4625ed5bad873c95208d54e1c003b2d/candles?interval=1m&limit=3"const res = await fetch("https://api.crius.sh/v1/robinhood/token/0x322f0929c4625ed5bad873c95208d54e1c003b2d/candles?interval=1m&limit=3");const { data, meta } = await res.json();import requests
r = requests.get( "https://api.crius.sh/v1/robinhood/token/0x322f0929c4625ed5bad873c95208d54e1c003b2d/candles?interval=1m&limit=3",)body = r.json()print(body["data"])Real response 200 Captured Oct 28, 2026 · 59 lines (arrays trimmed)
{ "data": { "bv": [ 26.708, 0 ], "c": [ 402.245, 402.686 ], "currency": "usd", "h": [ 402.245, 402.732 ], "interval": "1m", "l": [ 402.089, 402.245 ], "mc": [ 4632116, 4637185 ], "n": [ 2, 2 ], "o": [ 402.089, 402.245 ], "t": [ 1793225280000, 1793225400000 ], "v": [ 26.708, 1185.5 ] }, "meta": { "block": 100410168, "block_age_ms": 986, "block_ts": 1793225422000, "chain": "robinhood", "finalized_block": 100401329, "from_ts": 1793225280000, "lag_ms": 14, "note": "default (usd) = the token's real markets merged plus its launch curve; ?pool=main its main market only (the default with currency=quote), ?pool=<key> one pool, ?pool=all every pool. Candles open at the previous close (?continuous=false for first-trade opens); buckets without trades are omitted unless ?fill=true", "pools": [ "0xb2a6d67bf3fc511d11cac788a531655f5a7b60cd31c8ff386f0476121741e751", "0xa953ca88ff430e9487c60ca34d757414f4efda07", "0x8517f8071ae5b831b738052f12125e8e3d6c158b78728aa44ce3b25e5104d32e" ], "safe_block": 100405028, "to_ts": 1793225422000 }}What this tells you
- Columnar arrays: candle
iist[i],o[i],h[i],l[i],c[i],v[i]. Times are unix milliseconds; Lightweight Charts wants seconds (t / 1000). limit=3returned two candles: 1793225280000 (22:08 UTC) and 1793225400000 (22:10 UTC). Nobody traded at 22:09, so that bucket is missing. The stream won’t send it either; passfill=trueto REST if you want an evenly spaced axis.- The second candle opens at the first one’s close (402.245): candles open at the previous close, and the stream’s new candles do the same.
bvis 0 on the second candle: all 1,185.5 USD of its volume (v) was sells. Stream candles carryvbut notbvormc(market cap at close, here 4,637,185).meta.poolslists the three markets merged into these USD candles; thin side pools are left out so a dust trade can’t draw a wick.
What the candle channel sends
Subscribe with {"op": "subscribe", "channels": ["candles:0x322f0929c4625ed5bad873c95208d54e1c003b2d:1m"]}. Every trade of the token then pushes the candle it fell into:
| Field | Meaning |
|---|---|
t | Bucket start, unix ms. Same t as your last bar: replace it. Later t: a new bar |
o | Open: the previous candle's close |
h, l, c | High, low and close so far, in USD |
v | USD volume so far in this bucket |
n | Number of trades so far in this bucket |
A small trade far from the token’s current price (an off-market print in a thin pool, an arbitrage leg) counts in v and n but doesn’t move h, l or c, the same rule the REST candles use.
A chart relay in Node.js
The relay holds your key, subscribes to one candle channel, and pushes each update to every open page over Server-Sent Events. Pages load history through the relay too, with a short cache so a busy page doesn’t spend credits on every visit.
// candles-relay.mjs: CRIUS_API_KEY=... node candles-relay.mjs, then open http://localhost:8080import { createServer } from "node:http";import { readFileSync } from "node:fs";
const KEY = process.env.CRIUS_API_KEY;const API = "https://api.crius.sh/v1/robinhood";const TOKEN = "0x322f0929c4625ed5bad873c95208d54e1c003b2d"; // TSLA, Tesla tokenized stockconst INTERVAL = "1m";const CHANNEL = `candles:${TOKEN}:${INTERVAL}`;
const pages = new Set(); // open SSE responseslet last = null; // the forming candlelet history = { at: 0, body: "" }; // cached REST candleslet connectedBefore = false;
const send = (res, event, data) => res.write(`${event ? `event: ${event}\n` : ""}data: ${JSON.stringify(data)}\n\n`);
function connect(backoff = 1000) { 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: [CHANNEL] })); if (connectedBefore) { history.at = 0; // trades may have been missed: reload history for (const res of pages) send(res, "reload", {}); } connectedBefore = true; }; ws.onmessage = (e) => { touch(); const m = JSON.parse(e.data); if (m.ch === CHANNEL && !m.data.removed) { last = m.data; for (const res of pages) send(res, null, m.data); } else if (m.ch === "reorg") { history.at = 0; for (const res of pages) send(res, "reload", {}); } else if (m.ch === "error") { console.error(m.data); } }; ws.onclose = () => { clearTimeout(idle); setTimeout(() => connect(Math.min(backoff * 2, 30_000)), backoff + Math.random() * 500); }; ws.onerror = () => ws.close();}
createServer(async (req, res) => { if (req.url === "/candles") { if (Date.now() - history.at > 15_000) { const r = await fetch(`${API}/token/${TOKEN}/candles?interval=${INTERVAL}&limit=300`, { headers: { "x-api-key": KEY } }); history = { at: Date.now(), body: await r.text() }; } res.writeHead(200, { "content-type": "application/json" }); return res.end(history.body); } if (req.url === "/live") { res.writeHead(200, { "content-type": "text/event-stream", "cache-control": "no-cache", connection: "keep-alive" }); pages.add(res); if (last) send(res, null, last); req.on("close", () => pages.delete(res)); return; } res.writeHead(200, { "content-type": "text/html; charset=utf-8" }); res.end(readFileSync(new URL("./index.html", import.meta.url)));}).listen(8080);
connect();The page next to it draws the chart with TradingView Lightweight Charts, as in the chart example, and applies each streamed candle with update, which replaces the last bar or appends one:
<!doctype html><meta charset="utf-8" /><title>TSLA 1m live</title><div id="chart" style="height: 420px"></div><script type="module"> import { createChart, CandlestickSeries, HistogramSeries } from "https://unpkg.com/lightweight-charts@5.2.1/dist/lightweight-charts.standalone.production.mjs";
const chart = createChart(document.getElementById("chart"), { autoSize: true }); const candles = chart.addSeries(CandlestickSeries); const volume = chart.addSeries(HistogramSeries, { priceScaleId: "", priceFormat: { type: "volume" } }); volume.priceScale().applyOptions({ scaleMargins: { top: 0.8, bottom: 0 } }); const bar = (c) => ({ time: c.t / 1000, open: c.o, high: c.h, low: c.l, close: c.c });
async function load() { const { data } = await (await fetch("/candles")).json(); candles.setData(data.t.map((t, i) => bar({ t, o: data.o[i], h: data.h[i], l: data.l[i], c: data.c[i] }))); volume.setData(data.t.map((t, i) => ({ time: t / 1000, value: data.v[i] }))); }
await load(); const live = new EventSource("/live"); live.onmessage = (e) => { const c = JSON.parse(e.data); candles.update(bar(c)); // same time: replace the last bar; later time: append volume.update({ time: c.t / 1000, value: c.v }); }; live.addEventListener("reload", load);</script>-
History from REST, the forming bar from the stream. Call
/candlesonce (2 credits), draw it, then apply stream candles. Don’t rebuild history from the stream: it doesn’t replay old buckets. -
Reload after a gap. After a reconnect or a
reorgmessage, reload the last candles from REST. Reorged candle messages come back withremoved: true; skip them and let the reload fix the bar. -
One channel per interval. If users can switch between 1m and 1h, subscribe to the interval being shown and unsubscribe from the old one, so you stay within your plan’s subscriptions (10 on Free).
-
Format small prices. Meme tokens trade at fractions of a cent. Set the series’
priceFormatprecision from the last close so the axis stays readable.
Frequently asked questions
Which candle intervals can I stream?
1s, 30s, 1m, 5m, 15m, 1h, 4h, 6h and 1d, the same as the REST candles. Each interval of each token is its own channel (candles:<token>:1m), so it counts as one subscription.
Why don't I get a candle message every minute?
Candle messages are pushed by trades. A minute without trades sends nothing, and REST omits that bucket too unless you pass fill=true. There is also no snapshot on subscribe: draw the current candle from REST and wait for the next trade.
Why does a streamed candle differ slightly from the REST candle later?
The live candle is built as trades arrive, with the same rules as REST for opens and off-market prints. REST also applies filters that need trades after the one being judged, such as isolated spikes, and its default USD candles leave thin side pools out, while the live candle counts trades from every pool. Reload the last few candles from REST after a reconnect, or when a bucket closes if you need exact history.
Does the streamed candle include buy volume and market cap?
No. A stream candle has t, o, h, l, c, v (USD volume) and n (trades). Buy volume (bv) and market cap at close (mc) are in the REST candles.
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