The Solana Tracker solana ohlcv api returns candlesticks for any indexed token from GET /chart/{token}, or for one pool from GET /chart/{token}/{pool}, at intervals from 1 second to 1 month. In TypeScript, call getChartData or getPoolChartData with an interval, a time range in Unix seconds and an optional currency, and you get back an oclhv array of { open, close, low, high, volume, time }. Long ranges should be split into windows, and intervals with no trades can be missing from the series, so plan for both before you draw a chart.
This guide covers every documented parameter, the response shape, how to fetch long ranges in windows, how to detect and fill gaps, and how candles fit together with the live price stream. All calls run against the Solana Data API with the @solana-tracker/data-api SDK 0.5.0.
Solana OHLCV API parameters
getChartData(params) maps to GET /chart/{token}. getPoolChartData(params) maps to GET /chart/{token}/{pool}. Both take a ChartDataParams object:
| SDK field | Query parameter | Values | Default | Notes |
|---|---|---|---|---|
tokenAddress | path | Mint address | required | |
poolAddress | path | Pool address | none | Only for getPoolChartData |
type | type | 1s 5s 15s 1m 3m 5m 15m 30m 1h 2h 4h 6h 8h 12h 1d 3d 1w 1mn | server default | 1mn is one month |
timeFrom, timeTo | time_from, time_to | Unix seconds | none | Not milliseconds |
currency | currency | usd, eur, sol | usd | Unit of the open, high, low and close values |
marketCap | marketCap | boolean | false | Candles of market cap instead of price |
removeOutliers | removeOutliers | boolean | true | Keep the same setting across windows |
dynamicPools | dynamicPools | boolean | true | Token chart only: follows the main pool over time |
timezone | timezone | Abbreviation or IANA name, or "current" in the SDK | none | "current" uses the runtime's timezone |
fastCache | fastCache | boolean | false | Enables a live cache for faster responses |
Two details trip people up. Time values are seconds, while most of the rest of the Data API and Datastream use milliseconds. And the SDK only sends time_from and time_to when they are non-zero, so passing 0 means "no bound", not "the epoch".
Token chart or pool chart
getChartData (token) | getPoolChartData (pool) | |
|---|---|---|
| Endpoint | /chart/{token} | /chart/{token}/{pool} |
| Which trades | The token's main pool, chosen over time when dynamicPools is on | One pool only |
| Across a migration | Follows the main pool over time, so it can continue from a bonding curve to an AMM pool | Stops when that pool stops trading |
| Use for | Token pages, portfolio charts, screeners | Venue-specific analysis, comparing pools, backtests on one market |
For a launchpad token that migrates, the token chart is usually what users expect, because dynamicPools picks the main pool for each period. The pool chart is the right choice when your logic trades on a specific pool and you need its exact history.
Response shape
The response is an object with an oclhv array, not a bare array. Each candle's time is the start of the interval in Unix seconds.
{
"oclhv": [
{ "open": 150.12, "close": 150.44, "low": 149.86, "high": 151.04, "volume": 4123456.7, "time": 1791374400 },
{ "open": 150.44, "close": 150.65, "low": 149.65, "high": 151.25, "volume": 3810220.1, "time": 1791378000 }
]
}
The SDK types this as ChartResponse { oclhv: OHLCVData[] }. Some older snippets treat the response as an array and call .length on it; read oclhv instead. Sort by time after merging windows rather than relying on response order.
With marketCap: true, the four price fields are market cap values. With currency: "sol" or "eur", they are in that currency. Keep the unit in your cache key so a USD series and a SOL series never mix.
Fetch long ranges in windows
One request for 90 days of 5-minute candles is a large response and a single point of failure. Split the range instead:
- Convert the interval to seconds and choose a window size in candles (the example uses 1,000; this is a client-side choice, not a documented server limit).
- Request each window sequentially with the same
type,currency,marketCapandremoveOutliersvalues. - Merge into a map keyed by
time, which also removes duplicates where windows touch. - Sort ascending.
Put a cap on the number of requests. A typo such as 1s over 30 days would otherwise turn into thousands of calls, each counted against your plan's monthly allowance.
For a chart that loads more history as the user scrolls left, use the same idea in reverse: fetch the window that ends at the oldest candle you already have, merge and re-render.
Gaps and empty candles
Quiet tokens do not trade every interval, and intervals without trades can be absent from oclhv. The chart on solanatracker.io fills those gaps on the client before rendering. For your own app, decide explicitly:
| Strategy | What you do | When to use it |
|---|---|---|
| Leave gaps | Render only the candles you received | Analysis, backtests, anything that must not invent data |
| Fill flat | Insert candles with open = high = low = close = previous close and volume 0 | Chart libraries that expect a continuous time axis |
| Count only | Report the number and size of gaps | Data quality checks, alerting on stalled tokens |
Detect a gap by comparing consecutive time values against the interval length. Skip that check for 1mn, since months have no fixed length.
Never forward-fill across a range where the token was not indexed or did not exist yet. A flat line before launch looks like real price history.
Run the example
Create an empty directory and install the SDK:
npm init -y && npm pkg set type=module
npm install @solana-tracker/data-api@^0.5.0
npm install --save-dev tsx typescript @types/node
node --env-file=.env --import tsx index.ts
Add a .env file with your key from the Data API dashboard. TOKEN_MINT defaults to wrapped SOL, CHART_INTERVAL to 1h, CHART_DAYS to 7 and CHART_CURRENCY to usd.
ST_API_KEY=
TOKEN_MINT=So11111111111111111111111111111111111111112
POOL_ADDRESS=
CHART_INTERVAL=1h
CHART_DAYS=7
CHART_CURRENCY=usd
The script validates the inputs, fetches the range in windows with a timeout and retry on each request, merges and sorts the candles, counts gaps and prints the most recent candles.
import { Client, DataApiError, RateLimitError, ValidationError } from "@solana-tracker/data-api";
import type { ChartDataParams, OHLCVData } from "@solana-tracker/data-api";
const INTERVALS: Record<string, number | null> = {
"1s": 1, "5s": 5, "15s": 15, "1m": 60, "3m": 180, "5m": 300, "15m": 900, "30m": 1800,
"1h": 3600, "2h": 7200, "4h": 14400, "6h": 21600, "8h": 28800, "12h": 43200,
"1d": 86400, "3d": 259200, "1w": 604800, "1mn": null,
};
const WINDOW_CANDLES = 1000; // client-side window size, not a server limit
const MAX_REQUESTS = 20;
function fail(message: string): never {
console.error(message);
process.exit(1);
}
const apiKey = process.env.ST_API_KEY?.trim() || fail("Missing ST_API_KEY (https://www.solanatracker.io/account/data-api)");
const mint = process.env.TOKEN_MINT?.trim() || "So11111111111111111111111111111111111111112";
const pool = process.env.POOL_ADDRESS?.trim() || undefined;
const interval = process.env.CHART_INTERVAL?.trim() || "1h";
const days = Number(process.env.CHART_DAYS ?? 7);
const currency = (process.env.CHART_CURRENCY?.trim() || "usd") as "usd" | "eur" | "sol";
for (const address of [mint, pool]) {
if (address && !/^[1-9A-HJ-NP-Za-km-z]{32,44}$/.test(address)) fail(`Not a valid address: ${address}`);
}
if (!(interval in INTERVALS)) fail(`CHART_INTERVAL must be one of ${Object.keys(INTERVALS).join(", ")}`);
if (!["usd", "eur", "sol"].includes(currency)) fail("CHART_CURRENCY must be usd, eur or sol");
if (!Number.isFinite(days) || days <= 0) fail("CHART_DAYS must be a positive number");
const client = new Client({ apiKey });
const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));
async function withRetry<T>(fn: () => Promise<T>, attempts = 4, timeoutMs = 15_000): Promise<T> {
for (let attempt = 1; ; attempt++) {
let timer: NodeJS.Timeout | undefined;
try {
const timeout = new Promise<never>((_, reject) => {
timer = setTimeout(() => reject(new Error("chart request timed out")), timeoutMs);
});
return await Promise.race([fn(), timeout]);
} catch (error) {
const retryable = !(error instanceof ValidationError) &&
(!(error instanceof DataApiError) || error instanceof RateLimitError || (error.status ?? 500) >= 500);
if (!retryable || attempt >= attempts) throw error;
const retryAfterMs = error instanceof RateLimitError && error.retryAfter ? error.retryAfter * 1000 : 0;
await sleep(Math.max(Math.min(8_000, 500 * 2 ** (attempt - 1)), retryAfterMs));
} finally {
clearTimeout(timer);
}
}
}
const step = INTERVALS[interval] ?? null;
const to = Math.floor(Date.now() / 1000); // seconds, not ms
const from = to - Math.round(days * 86400);
const span = step ? step * WINDOW_CANDLES : to - from;
const windows: Array<[number, number]> = [];
for (let start = from; start < to; start += span) windows.push([start, Math.min(to, start + span - 1)]);
if (windows.length > MAX_REQUESTS) fail(`${windows.length} requests needed; use a larger interval or fewer days`);
try {
const byTime = new Map<number, OHLCVData>();
for (const [timeFrom, timeTo] of windows) {
const params: ChartDataParams = { tokenAddress: mint, type: interval, timeFrom, timeTo, currency };
const res = pool
? await withRetry(() => client.getPoolChartData({ ...params, poolAddress: pool }))
: await withRetry(() => client.getChartData(params));
for (const c of res.oclhv ?? []) if (Number.isFinite(c.time) && Number.isFinite(c.close)) byTime.set(c.time, c);
}
const candles = [...byTime.values()].sort((a, b) => a.time - b.time);
if (candles.length === 0) fail("No candles in this range (new, untraded or unindexed token)");
let missing = 0;
if (step) {
for (let i = 1; i < candles.length; i++) {
missing += Math.max(0, Math.round((candles[i]!.time - candles[i - 1]!.time) / step) - 1);
}
}
console.log(`${candles.length} ${interval} candles in ${currency.toUpperCase()} from ${windows.length} request(s), ${missing} missing interval(s)`);
console.table(candles.slice(-10).map((c) => ({
time: new Date(c.time * 1000).toISOString().slice(0, 16).replace("T", " "),
open: c.open, high: c.high, low: c.low, close: c.close, volume: Math.round(c.volume),
})));
} catch (error) {
const status = error instanceof DataApiError ? ` (HTTP ${error.status})` : "";
console.error(`Chart request failed${status}: ${error instanceof Error ? error.message : String(error)}`);
process.exitCode = 1;
}
The companion project adds market-cap candles, optional flat gap filling, a configurable window size and an aligned table with a range summary.
Combine candles with live prices
Candles are history. For the candle that is still open, combine the chart endpoint with the price stream:
- Load history with
getChartDataand render it. - Subscribe to the aggregated price room and update the last candle's high, low and close on each message; the Solana price WebSocket guide shows the subscription and reconnect handling.
- When a message falls into a new interval, close the current candle and open a new one at that price.
- After a reconnect, refetch the last few candles from REST and replace your locally built ones.
Price messages carry no volume, so the volume of the open candle is either left at the REST value or summed from the trades WebSocket. Two open-source references implement this end to end: solana-liveline-chart-example and solana-tradingview-advanced-chart-example on the Solana Tracker GitHub.
Production pitfalls
Seconds versus milliseconds. time_from, time_to and candle time are Unix seconds. Datastream timestamps and lastUpdated fields are milliseconds. Convert at the boundary and name variables with their unit.
Keep filters stable across windows. Mixing removeOutliers settings, currencies or marketCap between windows produces a series that looks continuous and is not. Treat those values as part of the cache key.
Cache closed candles. A candle whose interval has ended does not change. Cache it by token, pool, interval, currency, market-cap flag and time, and only refetch the open candle and the last one or two closed ones.
Do not invent history. Fill gaps only for display, and only between real candles. Report the number of gaps so a stalled token is visible.
Volume units. The docs do not spell out the unit of volume for each currency setting. Label it as reported by the API rather than assuming USD, and compare volumes only within one series.
Timezones change daily candles. Daily and weekly candles depend on the timezone parameter. Pick one, usually UTC for backends, and pass it consistently.
Sub-minute intervals over long ranges. 1s candles over days are a lot of data. Use them for short windows and move to coarser intervals as the user zooms out.
FAQ
What intervals does the chart endpoint support?
1s, 5s, 15s, 1m, 3m, 5m, 15m, 30m, 1h, 2h, 4h, 6h, 8h, 12h, 1d, 3d, 1w and 1mn (one month), passed as type.
Can I get candles in SOL or EUR instead of USD?
Yes. Set currency to sol or eur. The default is usd. Set marketCap: true for market cap candles instead of price candles.
Why are some candles missing?
Intervals without trades can be absent from the response, especially on quiet tokens and short intervals. Detect gaps by comparing consecutive time values and fill them flat only when your chart library needs a continuous axis.
How do I chart one specific pool?
Call getPoolChartData with tokenAddress and poolAddress, which maps to GET /chart/{token}/{pool}. The token chart instead follows the main pool over time when dynamicPools is on.
How far back can I request candles?
As far back as the token's indexed history. Request long ranges in windows rather than in one call, and cap the number of requests on your side.
References
- Get Solana token prices and OHLCV charts (guide)
- Get OHLCV data for a token
- Get OHLCV data for a token and pool pair
- Stream Solana token prices and update live charts (guide)
- solana-liveline-chart-example on GitHub
- solana-tradingview-advanced-chart-example on GitHub
- @solana-tracker/data-api on npm
Companion project
The companion project fetches candles for TOKEN_MINT or POOL_ADDRESS in windows, merges and dedupes them, reports or fills gaps, and prints the latest candles with a range summary. It supports CHART_INTERVAL, CHART_DAYS, CHART_CURRENCY, MARKET_CAP and FILL_GAPS.
cp .env.example .env && npm install && npm start