How to Fetch Historical OHLCV Candles on Robinhood Chain
Fetch historical OHLCV candles for any Robinhood Chain token: intervals from 1s to 1d, paging with to_ts, pool selection, gap filling and clean wicks.
Crius 8 min read
Short answer: call GET /v1/robinhood/token/{address}/candles?interval=1h on the Crius API to get open, high, low, close and volume for any Robinhood Chain token, from 1-second to 1-day buckets, back to the token’s first trade. For longer histories, page backwards with to_ts; for charts, add fill=true so empty buckets don’t leave gaps.
This guide covers the request, the response format, paging through months of history, choosing which pools a candle is built from, and the data-quality rules that keep dust trades and off-market prints out of your highs and lows. Every code sample was run against the live API while writing this (October 2026).
The request
curl "https://api.crius.sh/v1/robinhood/token/0x322f0929c4625ed5bad873c95208d54e1c003b2d/candles?interval=1h&limit=3" \ -H "x-api-key: $CRIUS_API_KEY"That token is TSLA, Tesla’s tokenized stock on Robinhood Chain. The parameters:
| Parameter | Values | Default | Notes |
|---|---|---|---|
interval |
1s, 30s, 1m, 5m, 15m, 1h, 4h, 6h, 1d |
1m |
Bucket size. Buckets are aligned to UTC. |
limit |
1–1500 | 500 | Number of buckets. |
from_ts, to_ts |
Unix milliseconds | the last limit buckets |
Window to return. |
currency |
usd, quote |
usd |
quote prices in the main pool’s quote token (ETH, WETH, USDG …). |
pool |
markets, main, all, or a pool key |
markets (usd) / main (quote) |
Which pools the candles are built from. |
continuous |
true, false |
true |
Open each candle at the previous close. |
fill |
true, false |
false |
Emit flat candles for buckets with no trades. |
Without from_ts and to_ts, you get the most recent limit buckets, ending with the current (still open) one.
The response: columnar arrays
Candles come back as parallel arrays rather than an array of objects. It is smaller over the wire and maps directly onto charting libraries:
{ "data": { "interval": "1h", "currency": "usd", "t": [1792828800000, 1792832400000, 1792836000000], "o": [371.193, 370.843, 371.239], "h": [373.614, 373.488, 372.938], "l": [369.41, 369.085, 369.888], "c": [370.843, 371.239, 370.268], "v": [15932.4, 23648.5, 13459.2], "bv": [1014.77, 4598.04, 10643.7], "n": [77, 107, 81], "mc": [4344418, 4349056, 4337675] }, "meta": { "block": 96713540, "lag_ms": 21, "from_ts": 1792828800000, "pools": ["0xb2a6…", "0xa953…", "…"] }}t: bucket start, Unix milliseconds (UTC).o,h,l,c: prices in USD (or the quote token withcurrency=quote).v: volume in USD;bv: the buy side of it, so sell volume isv - bv.n: number of trades in the bucket.mc: market cap at the close (USD candles only).meta.pools: exactly which pools went into the candles, so you can audit any bar.
Turning the columns into rows is one line in most languages:
const bars = d.t.map((t, i) => ({ time: t / 1000, open: d.o[i], high: d.h[i], low: d.l[i], close: d.c[i], volume: d.v[i] }));Paging through months of history
One call returns at most 1,500 buckets: about 25 hours of 1-minute candles, 62 days of hourly candles or four years of daily ones. For more, walk backwards: ask for the newest page, then set to_ts to just before the oldest bucket you received, and repeat.
// Fetch N days of hourly candles, paging backwards with to_ts, and write a CSV.const API = "https://api.crius.sh/v1/robinhood";const KEY = process.env.CRIUS_API_KEY;const TOKEN = "0x322f0929c4625ed5bad873c95208d54e1c003b2d"; // TSLAconst STEP_MS = 3_600_000, DAYS = 60;
async function page(toTs) { const url = `${API}/token/${TOKEN}/candles?interval=1h&limit=1000&to_ts=${toTs}`; for (let attempt = 0; attempt < 5; attempt++) { const res = await fetch(url, { headers: { "x-api-key": KEY } }); if (res.status === 429 || res.status === 503) { await new Promise((r) => setTimeout(r, Number(res.headers.get("retry-after") ?? 2) * 1000)); continue; } if (!res.ok) throw new Error(`${res.status} ${await res.text()}`); return (await res.json()).data; } throw new Error("gave up after retries");}
const since = Date.now() - DAYS * 86_400_000;const rows = new Map(); // keyed by bucket start: dedupes any overlap between pageslet to = Date.now();while (to > since) { const d = await page(to); if (d.t.length === 0) break; // reached the first trade d.t.forEach((t, i) => rows.set(t, [t, d.o[i], d.h[i], d.l[i], d.c[i], d.v[i], d.n[i]])); to = d.t[0] - STEP_MS; // next page ends just before the oldest bucket}const out = [...rows.values()].filter((r) => r[0] >= since).sort((a, b) => a[0] - b[0]);console.log(`${out.length} hourly candles`);Run against TSLA it returned 1,439 hourly candles covering 60 days in two requests. Three details matter:
- Key by
t. Pages can overlap by one bucket at the boundary; a map keyed by bucket start removes duplicates. - Stop on an empty page. Before a token’s first trade there is nothing to return.
- Respect
Retry-After. On a 429 (rate limit) or 503 (busy), wait the number of seconds in the header and retry the same page.
The same loop in Python:
import os, time, requests
API = "https://api.crius.sh/v1/robinhood"HEADERS = {"x-api-key": os.environ["CRIUS_API_KEY"]}TOKEN = "0x322f0929c4625ed5bad873c95208d54e1c003b2d"
def candles(interval="1h", limit=1000, to_ts=None, **extra): params = {"interval": interval, "limit": limit, **extra} if to_ts: params["to_ts"] = to_ts for _ in range(5): r = requests.get(f"{API}/token/{TOKEN}/candles", params=params, headers=HEADERS, timeout=30) if r.status_code in (429, 503): time.sleep(float(r.headers.get("retry-after", 2))) continue r.raise_for_status() d = r.json()["data"] return [dict(t=t, o=o, h=h, l=l, c=c, v=v) for t, o, h, l, c, v in zip(d["t"], d["o"], d["h"], d["l"], d["c"], d["v"])] raise RuntimeError("rate limited")
bars = candles("1d", limit=30)print(len(bars), "daily bars; last:", bars[-1])Each page costs 2 credits regardless of how many buckets it holds, so fetching a 60-day hourly history costs 4 credits. Store candles you have already fetched: closed buckets never change, except after a chain reorganisation within the last few minutes.
Which pools a candle is built from
Most active tokens trade in several pools at once: Uniswap v2, v3 and v4, against ETH, WETH or USDG, sometimes against another token. How you combine them changes the chart.
pool=markets(default for USD): the token’s real markets merged. A pool qualifies when it holds at least $1,000 or 2% of the token’s liquidity and its own price agrees with the token’s price within 25%. Thin side pools and pools priced through a junk or manipulated quote token are left out. Merging gives denser candles and a more accurate USD price than any single pool.pool=main: only the token’s pricing pool, the pair most DEX screeners chart. This is the default when you ask forcurrency=quote, because quote prices only make sense within one pair.pool=all: every pool, including thin ones. Useful for research, noisy for charts.pool=<key>: one specific pool: a v2/v3 pool address or a 32-byte v4 pool id.
The merged view matters most for tokens whose deepest pool is quoted in something other than ETH or USDG. TSLA’s pricing pool, for example, quotes it against another tokenized stock; charting that pool alone in USD would carry the other stock’s moves into the TSLA chart, and it trades far less often than the merged markets.
Clean highs and lows
Raw on-chain swaps produce some prices that are not market prices. Crius filters two kinds before they reach a candle’s high or low; both still count toward volume and trade counts:
- Dust swaps. A trade where either side is under 10,000 raw units, for example 1 wei for 1 wei, has a meaningless price: a 1:1 raw swap between two 18-decimal tokens prices one at a whole ETH. These never set a price.
- Isolated spikes. A trade under $1,000 whose price is more than 1.3× away from both the five trades before it and the five after it, while those agree with each other, is an off-market fill: an arbitrage leg, a sandwich, a thin-pool print. The price snapped straight back, so it is left out of the high and low.
Large trades always count, because real price impact is real. A $380,000 dump that recovers within the same five minutes still draws its wick.
Continuous candles and gap filling
By default each candle opens at the previous candle’s close (continuous=true), the way exchange and DEX screener charts draw them. Without it, a token that trades once every few minutes produces bodies that float apart; set continuous=false if you want the first trade of each bucket as the open.
Buckets with no trades are omitted unless you pass fill=true, which inserts flat candles (open = high = low = close = the last close, zero volume) between the first and last returned bucket. Charting libraries such as TradingView Lightweight Charts compress missing time otherwise, so a sparse token’s x-axis jumps unevenly.
Fill works between trades you have: a window with no trades at all returns no candles even with fill=true. If you need a value for an empty window, use the last close from the previous page or the token’s current price from /price.
Prices in the quote token
currency=quote returns candles in the main pool’s quote asset instead of USD. For an ETH-quoted memecoin you get its price in ETH, which removes ETH’s own moves from the chart. Since quote prices only make sense within one pair, this mode uses the main pool unless you name another.
curl "https://api.crius.sh/v1/robinhood/token/0x322f0929c4625ed5bad873c95208d54e1c003b2d/candles?interval=15m¤cy=quote&limit=4" \ -H "x-api-key: $CRIUS_API_KEY"Choosing an interval
| Use | Interval | One page covers |
|---|---|---|
| Live chart, last hour | 1s / 30s |
25 min / 12.5 h |
| Intraday chart | 1m / 5m |
25 h / 5 days |
| Swing chart, backtests | 15m / 1h |
15 days / 62 days |
| Long-term history | 4h / 6h / 1d |
250 days / 1 year / 4 years |
For a chart that stays current, fetch history once over REST and then subscribe to candles:<token>:1m on the WebSocket stream, replacing the last bar whenever an update for the same t arrives. The trading terminal guide walks through exactly that.
Frequently asked questions
How far back do candles go? To each token’s first trade. Crius indexed Robinhood Chain from block zero, so the full history is available for every token, not only for recent ones.
Why does a candle’s open differ from its first trade?
With the default continuous=true the open is the previous close. Pass continuous=false to get the first trade’s price instead.
Why do my USD candles differ from one DEX’s chart?
DEX screeners usually chart a single pair. Crius merges the token’s real markets by default. Use pool=main to match a single-pair chart.
What does a candle request cost? 2 credits per call, whatever the interval or number of buckets. Anonymous requests work too, at 2 requests per second per IP.
Are candles available in real time?
Yes. The current bucket is included and updates with every block; for push updates, subscribe to candles:<token>:<interval> on the WebSocket stream, available on every plan with a key.
Next steps
- API reference: Trades and candles and the chart example with working code.
- Product overview: OHLCV candles API, historical data and token prices.
- Related guides: real-time token prices and building a trading terminal.
Get a free key at dashboard.crius.sh; questions go to support@crius.sh.
Build with Crius
Robinhood Chain data in one API
Free plan with 1M credits a month. Your key in under a minute.
More from the blog
- HoldersSnapshots
Robinhood Chain Token Holders API: Live and Historical Snapshots
Get the holders of any Robinhood Chain token, live or at any past block or market cap, with cohorts and holder-count history. Code in Node.js and Python.
- TutorialHistorical data
How to Get All Robinhood Chain Transactions Without Running a Node
Export every trade, transfer and liquidity event for a Robinhood Chain token or wallet with an indexed API. Tested Python and Node.js, no archive node.
- TutorialNode.js
How to Connect an LLM to Robinhood Chain Data
Three working ways to give a language model live Robinhood Chain data: an MCP server, a tool-calling loop for any model API, and retrieval over the Crius docs.