CRIUS Docs
API status Get API key
Real-time

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.

Updated 2 min read
Short answer

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?

GET/v1/robinhood/token/0x322f0929c4625ed5bad873c95208d54e1c003b2d/candles?interval=1m&limit=3
curl "https://api.crius.sh/v1/robinhood/token/0x322f0929c4625ed5bad873c95208d54e1c003b2d/candles?interval=1m&limit=3"
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 i is t[i], o[i], h[i], l[i], c[i], v[i]. Times are unix milliseconds; Lightweight Charts wants seconds (t / 1000).
  • limit=3 returned 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; pass fill=true to 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.
  • bv is 0 on the second candle: all 1,185.5 USD of its volume (v) was sells. Stream candles carry v but not bv or mc (market cap at close, here 4,637,185).
  • meta.pools lists the three markets merged into these USD candles; thin side pools are left out so a dust trade can’t draw a wick.
OHLCV candles reference Runs from your browser on the anonymous plan Data looks wrong? Report it

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:

FieldMeaning
tBucket start, unix ms. Same t as your last bar: replace it. Later t: a new bar
oOpen: the previous candle's close
h, l, cHigh, low and close so far, in USD
vUSD volume so far in this bucket
nNumber 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:8080
import { 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 stock
const INTERVAL = "1m";
const CHANNEL = `candles:${TOKEN}:${INTERVAL}`;
const pages = new Set(); // open SSE responses
let last = null; // the forming candle
let history = { at: 0, body: "" }; // cached REST candles
let 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>
  1. History from REST, the forming bar from the stream. Call /candles once (2 credits), draw it, then apply stream candles. Don’t rebuild history from the stream: it doesn’t replay old buckets.

  2. Reload after a gap. After a reconnect or a reorg message, reload the last candles from REST. Reorged candle messages come back with removed: true; skip them and let the reload fix the bar.

  3. 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).

  4. Format small prices. Meme tokens trade at fractions of a cent. Set the series’ priceFormat precision 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.

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