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_solandvolume_tokenon each candle are the sum of actual swap amounts within the bar. - Trade counts are real.
trade_count,buy_count,sell_count, andunique_tradersare 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_solis lamports — divide by1e9to get SOL (12340000000→12.34 SOL).volume_tokenis the token’s base units — divide by10 ** quote_decimalsfor 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:
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.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.
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_dbcandmeteora_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-feepumpfuntrades by the same program from beforeagent_tradeexisted; - on dust trades, where the smaller leg (
sol_amountortoken_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.
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 fromprice_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.
