Solana Tracker LogoSolana Tracker
Swap
Developers
⌘K
Affiliate
All Resources
Solana OHLCV API: Candles, Intervals and Gaps
Data APITypeScriptJune 4, 202612 min readUpdated October 7, 2026Solana Tracker

Solana OHLCV API: Candles, Intervals and Gaps

Fetch candles from the Solana OHLCV API in TypeScript: intervals, time ranges, pool charts, currency, splitting long ranges and handling missing candles.

  • ohlcv,
  • charts,
  • data-api,
  • rest,
  • typescript
Solana Data API›

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 fieldQuery parameterValuesDefaultNotes
tokenAddresspathMint addressrequired
poolAddresspathPool addressnoneOnly for getPoolChartData
typetype1s 5s 15s 1m 3m 5m 15m 30m 1h 2h 4h 6h 8h 12h 1d 3d 1w 1mnserver default1mn is one month
timeFrom, timeTotime_from, time_toUnix secondsnoneNot milliseconds
currencycurrencyusd, eur, solusdUnit of the open, high, low and close values
marketCapmarketCapbooleanfalseCandles of market cap instead of price
removeOutliersremoveOutliersbooleantrueKeep the same setting across windows
dynamicPoolsdynamicPoolsbooleantrueToken chart only: follows the main pool over time
timezonetimezoneAbbreviation or IANA name, or "current" in the SDKnone"current" uses the runtime's timezone
fastCachefastCachebooleanfalseEnables 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 tradesThe token's main pool, chosen over time when dynamicPools is onOne pool only
Across a migrationFollows the main pool over time, so it can continue from a bonding curve to an AMM poolStops when that pool stops trading
Use forToken pages, portfolio charts, screenersVenue-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:

  1. 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).
  2. Request each window sequentially with the same type, currency, marketCap and removeOutliers values.
  3. Merge into a map keyed by time, which also removes duplicates where windows touch.
  4. 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:

StrategyWhat you doWhen to use it
Leave gapsRender only the candles you receivedAnalysis, backtests, anything that must not invent data
Fill flatInsert candles with open = high = low = close = previous close and volume 0Chart libraries that expect a continuous time axis
Count onlyReport the number and size of gapsData 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:

  1. Load history with getChartData and render it.
  2. 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.
  3. When a message falls into a new interval, close the current candle and open a new one at that price.
  4. 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
View source on GitHub›Run in StackBlitz›

Runnable Node.js project — clone from GitHub, add keys to .env, then npm start. Use a private local environment for credentials. Check the companion package version before running.

Related Guides

Solana Token Price API: Pools, Batches and History
Data API

Solana Token Price API: Pools, Batches and History

Read more
Solana Liquidity Events API: LP History and Live Stream
Data API

Solana Liquidity Events API: LP History and Live Stream

Read more
Jupiter DCA API: Track Recurring Orders and Live Fills
Data API

Jupiter DCA API: Track Recurring Orders and Live Fills

Read more

Products

  • Data API
  • Pump.fun API
  • Solana RPC
  • Dedicated Nodes
  • Yellowstone gRPC
  • Raptor Swap API
  • Enterprise

Trading

  • Swap
  • Latest Tokens
  • Trending
  • Top Gainers
  • Memescope
  • Whale Watch
  • KOL Tracker

Tools

  • Wallet Tracker
  • Rugcheck
  • PnL Leaderboard
  • KOLScan
  • Axiom Leaderboard
  • Photon Leaderboard
  • Bloom Leaderboard
  • FOMO Leaderboard
  • GMGN Leaderboard
  • Pump.fun App Leaderboard
  • Terminal Leaderboard
  • Platform Compare
  • My Positions
  • Teams

Resources

  • Developer Guides
  • Blog
  • Documentation
  • API Reference
  • Status
  • Affiliate Program — 25% recurring, uncapped
Solana TrackerSolana Tracker© 2026
Terms of ServicePrivacy PolicyContact