# 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.

Source: https://docs.crius.sh/guides/stream-live-candles/ · Updated 2026-10-28

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.

[GET `/token/{address}/candles`](https://docs.crius.sh/reference/trades/ohlcv-candles/) [GET `/token/{address}/trades`](https://docs.crius.sh/reference/trades/trade-tape/)

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](https://docs.crius.sh/reference/trades/ohlcv-candles/) 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

Example [Anonymous · no key · 2 credits](https://docs.crius.sh/reference/trades/ohlcv-candles/)

### 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**

```bash
curl "https://api.crius.sh/v1/robinhood/token/0x322f0929c4625ed5bad873c95208d54e1c003b2d/candles?interval=1m&limit=3"
```

**JavaScript**

```js
const res = await fetch("https://api.crius.sh/v1/robinhood/token/0x322f0929c4625ed5bad873c95208d54e1c003b2d/candles?interval=1m&limit=3");
const { data, meta } = await res.json();
```

**Python**

```python
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)**

```json
{
  "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](https://docs.crius.sh/reference/trades/ohlcv-candles/) Runs from your browser on the anonymous plan [Data looks wrong? Report it](mailto:support@crius.sh?subject=Wrong%20data%20in%20docs%20example%3A%20TSLA%20one-minute%20candles%2C%20with%20a%20gap&body=Page%3A%20https%3A%2F%2Fdocs.crius.sh%2Fguides%2Fstream-live-candles%2F%23history%0ARequest%3A%20GET%20%2Fv1%2Frobinhood%2Ftoken%2F0x322f0929c4625ed5bad873c95208d54e1c003b2d%2Fcandles%3Finterval%3D1m%26limit%3D3%0A%0AWhat's%20wrong%20or%20missing%3F%0A%0A)

## 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.

```js
// 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](https://github.com/tradingview/lightweight-charts), as in the [chart example](https://docs.crius.sh/examples/chart/), and applies each streamed candle with `update`, which replaces the last bar or appends one:

```html
<!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.
    

Want every trade, not just the candle?

Subscribe to `trades:<token>` on the same connection to show a live trade list under the chart. The [trades stream guide](https://docs.crius.sh/guides/stream-trades-websocket/) has a client that resumes and backfills.

## 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

[Guide **OHLCV candles** Robinhood Chain OHLCV API: candles for any token from 1 second to 1 day, with buy volume, trade count and market cap per candle. Real examples and chart code. Read](https://docs.crius.sh/guides/get-ohlcv-candles/) [Docs **Chart example** Draw a live candlestick and volume chart for any Robinhood Chain token with the Crius candles endpoint and TradingView Lightweight Charts, with full source code. Read](https://docs.crius.sh/examples/chart/) [API reference **OHLCV candles API** Candles in columnar form (one array per field, aligned by index), which is compact and maps straight onto charting libraries. Read](https://docs.crius.sh/reference/trades/ohlcv-candles/) [API reference **WebSocket stream** Real-time Robinhood Chain data over one WebSocket - trades, prices, live candles, new launches, graduations, new pools, LP and tax changes, wallet activity, with replay and reorg handling. Read](https://docs.crius.sh/reference/stream/) [Guide **Stream trades** Stream every trade of a Robinhood Chain token, pool or wallet over the Crius WebSocket, with Node.js and Python clients that reconnect, resume and backfill. Read](https://docs.crius.sh/guides/stream-trades-websocket/)
