Skip to main content
Most Solana OHLCV APIs poll pool account state every block or two and build a candle from those snapshots. The price comes from reserves_a / reserves_b, the volume comes from differencing reserves between snapshots. Dexploit doesn’t do that. We process every swap event from the Yellowstone gRPC stream as it arrives, normalize it across protocols (Pump.fun, PumpSwap, Raydium, Meteora), and aggregate the events into bars.

Why this matters for you

Building from events instead of state changes a few things you can rely on:
  • Volume is exact. volume_sol and volume_token on each candle are the sum of actual swap amounts within the bar.
  • Trade counts are real. trade_count, buy_count, sell_count, and unique_traders are direct counts. State-snapshot APIs can’t produce these.
  • No phantom bars. When a pool has zero trades during a 1-minute interval, that interval has no bar. We don’t carry-forward the previous close.
  • Bar boundaries are clean. 1-minute bars start at :00. RPC drops or block-time jitter don’t shift them.

What’s in a candle

base_mint is always wrapped SOL — prices are quoted in SOL.
Both volume_sol and volume_token are integers in base units, despite the names:
  • volume_sol is lamports — divide by 1e9 to get SOL (12340000000 → 12.34 SOL).
  • volume_token is the token’s base units — divide by 10 ** quote_decimals for the human-readable amount.

Prices on a candle

open, high, low, and close are in SOL per whole token, already decimals-adjusted. They are computed when the bar closes and served back exactly as stored — a bar is never re-derived at read time.

Which price field should I use?

A swap carries three numbers that look like a price. They mean different things, and only one of them is right for every venue and every date.

Start with price_per_token

price_per_token is the price the trade actually executed at, computed from the trade’s own two legs and nothing else — it is exactly sol_amount / token_amount. Because it never touches pool reserves, nothing about how a venue reports its reserves can bias it, which is what makes it the one price field that is correct on all ten venues and across all of history. It arrives as lamports per atomic token unit. Scale it once to get SOL per whole token:
The streaming feeds don’t carry the field, but they carry both legs, so the same number is sol_amount / token_amount from the frame you already have.
base_decimals and quote_decimals are the reverse of the usual convention. base_decimals is the SOL side and is 9 on every venue. quote_decimals holds the token’s own decimals, despite the name — commonly 6, but 9 and other values are normal. Read quote_decimals as “the token’s decimals” and the line above is the whole conversion.
For a USD price, multiply by the SOL/USD rate.

pool_price is a reserve ratio, not a quote

pool_price is the ratio of the pool’s reported SOL reserve to its reported token reserve, already scaled to SOL per whole token, and passed through as the venue published it. We serve it uncorrected on purpose: it is the faithful record of what the venue reported, which is what makes it useful for reserve and liquidity work. That same property is why it isn’t a price you can trade against:
  • On constant-product and bonding-curve pools the ratio is the pool’s marginal price, and it lands within about a fee of the executed price.
  • On concentrated-liquidity pools the reported reserves are the pool’s whole inventory across every tick or bin — not the liquidity at the active price. The ratio is an inventory statistic, and it can sit far from anything you could fill at.
  • On PumpSwap pools that graduated from the pump.fun bonding curve after mid-July 2026, about 17.58 SOL of quote sits outside the account whose balance the swap event reports. The ratio therefore understates the price by (sol_reserve + 17.58) / sol_reserve — under a percent on a well-funded pool, and much larger as the pool drains.
Per-venue numbers are in Data coverage.

price_impact_bps is null where it wasn’t computed

price_impact_bps is nullable on every surface that carries it. REST (/swaps* and /swaps/{signature}), GraphQL (Int, previously Int!) and WebSocket all return null where no impact was computed. (The gRPC streams don’t carry the field.) None of them returns 0 for that case, so on REST and GraphQL a 0 is a real reading. The WebSocket exception is below. It’s computed on these venues: It’s null:
  • on meteora_dbc and meteora_pools;
  • on the six venues in the second row, for swaps from before that venue started computing it, and for swaps that couldn’t be priced;
  • on trades made by pump.fun’s own Mayhem program (agent_trade = 2), and on older zero-fee pumpfun trades by the same program from before agent_trade existed;
  • on dust trades, where the smaller leg (sol_amount or token_amount, in base units) is under 10,000. At that size one unit of integer rounding moves the executed price by more than 1 bp, so any number would be rounding noise.
In rare cases a stream frame carries 0 for a swap that REST and GraphQL serve as null: a swap on one of the six venues in the second row whose computed value landed on exactly 0. Treat both as unknown. On pumpswap the comparison is corrected for the SOL a pump.fun-migrated pool holds outside the account the swap reports, so it does not inherit the understatement described for pool_price. The same correction is applied on REST, GraphQL and WebSocket, so the surfaces agree for a given swap, apart from the rare 0 above and one more documented limit: REST and GraphQL clamp price_impact_bps to the Int16 range (−32,768 to 32,767), while the WebSocket feed carries the full value, so beyond that range they differ by design. It measures impact relative to the pool, not slippage against an external reference — for that, compare price_per_token to a reference you control.

For a price series, use candles

Candle prices are written when the bar closes and served back verbatim — stored bars are never re-derived at read time. So a change in how a venue’s price is derived reaches bars written from the day it ships forward; bars that closed earlier keep the value they were written with. One case is worth a number. On PumpSwap, daily closes written before the fix shipped carry the reserve-ratio understatement described above. Over the seven days 2026-08-31 to 2026-09-06 inclusive, across 153,174 PumpSwap daily bucket-closes, 39.5% (60,474) would take a different value once the migrated pool’s off-vault SOL is accounted for. Most of those moves are small; the largest single one in that window was 135×, on a pool drained to near-zero SOL, where the fixed shortfall dominates whatever is left. When you need a price you can act on, derive it from price_per_token over the swaps in the window rather than reading the bar close.

Comparison with the account-update model

Getting the data

GET /api/v1/candles for a time range, GET /api/v1/candles/latest for the most-recent N bars. Both require pair_address, not token mint. For real-time bars and ticks, subscribe over WebSocket.