Reading Solana Pool State Instead of Trusting a Quote

A quote is an API's opinion from a slot that has passed. Reading reserves directly tells you what the pool will actually do to your trade.

BoltTx Team··9 min read
solanapoolammreservesprice-impacttrading-bot

Most bots ask an aggregator what a trade is worth and act on the answer. That works until it does not, and when it fails you have no way to tell whether the quote was wrong or the market moved.

★A quote is a number from a slot that has already passed. The pool's reserves are what your transaction will actually execute against.★

What a Constant-Product Pool Does

Almost every AMM you trade against holds two reserves and preserves their product:

x · y = k

Your trade moves along that curve, and the output follows directly from the reserves:

function amountOut(amountIn: bigint, reserveIn: bigint, reserveOut: bigint, feeBps: bigint) {
  const inAfterFee = amountIn * (10_000n - feeBps) / 10_000n;
  return (inAfterFee * reserveOut) / (reserveIn + inAfterFee);
}

★That is the whole pricing model for a constant-product pool.★ Given the two reserves and the fee, you can compute the output yourself — no quote API involved, no round trip, no staleness beyond the slot you read at.

Two things follow immediately:

The price you get depends on your own size, not on a market price that exists independently of you.

★You can compute the exact same number the program will compute★, which means you can verify a quote rather than trusting it.

Reading the Reserves

The reserves are token accounts owned by the pool, so reading them is a standard balance read:

const [vaultA, vaultB] = await connection.getMultipleAccountsInfo([
  poolVaultA, poolVaultB,
]);

const reserveA = vaultA.data.readBigUInt64LE(64);   // ★amount at offset 64★
const reserveB = vaultB.data.readBigUInt64LE(64);

★One getMultipleAccountsInfo call gets both, atomically from the same slot.★ Reading them in two separate calls can straddle a slot boundary and give you a mismatched pair — reserves that never existed together, producing a price that was never real.

The vault addresses come from the pool account, which you decode once and cache. Its layout is program-specific, and the reliable way to get it is the program's IDL rather than a memorised offset.

If your pricing is right and transactions still miss, a free BoltTx key is one line to test the submission path.

Why This Beats a Quote for a Bot

No round trip. A quote API call is network time in your hot path. Reserves you already stream cost nothing at decision time.

No staleness you cannot measure. ★A quote does not tell you which slot it came from.★ A reserve read does, via getAccountInfoAndContext.

const { context, value } = await connection.getAccountInfoAndContext(poolVaultA);
console.log("reserves from slot", context.slot);

You can verify. Comparing your computed output against a quote is how you discover an aggregator is routing you somewhere worse than it claims.

It works when the API does not. Rate limits, outages, and unsupported new tokens all break a quote-dependent bot while leaving a reserve-reading one working.

★The honest exception: aggregators genuinely add value on multi-hop routing across many venues.★ Reading reserves yourself is the better tool for a specific pool you have already chosen; it is not a replacement for route discovery across dozens of them.

Stream Instead of Polling

Pool accounts change every time someone trades, which makes them a poor fit for polling:

connection.onAccountChange(poolVaultA, (info, ctx) => {
  reserves.a = info.data.readBigUInt64LE(64);
  reserves.slot = ctx.slot;
}, "processed");

processed is correct here — you are deciding, not recording.★ The worst case for a rolled-back reserve read is a wasted attempt, while waiting for confirmed means deciding on state that is a slot or more old.

Track the slot alongside the value. A reserve figure without a slot cannot be checked for staleness, and staleness is the whole reason you are reading directly.

The Failure Mode Worth Knowing

★Reading reserves does not protect you from the pool changing between your read and your execution.★ It cannot — nothing can.

slot N     you read reserves, compute output
slot N+1   someone else trades, reserves move
slot N+2   ★your transaction executes against the new reserves★

Your computed output was correct for slot N and irrelevant for slot N+2. This is exactly what the minimum-output check exists for, and it is why reading reserves does not replace setting one.

The two work together: ★reserves tell you what to expect and how to size, the on-chain minimum protects you when expectation and reality diverge.★

Sanity Checks Worth Running

if (reserveA === 0n || reserveB === 0n) return null;    // ★drained or uninitialised★

const impact = Number(spotOut - expectedOut) / Number(spotOut);
if (Math.abs(impact) > MAX_IMPACT) return null;         // ★your size is too large★

A zero reserve is common on new tokens and produces a division that is either an error or a nonsensical price depending on your code path.

★An impact check is the one people skip, and it is the one that catches a pool too thin for your intended size.★ A trade that moves the price twenty percent against you will execute — the program has no opinion about whether that was wise.

What Landing Looks Like

Real transactions through our delivery nodes: median confirmation 336ms — under one slot.

★Every slot between reading reserves and executing is a slot for someone else to trade first.★ Reading directly removes the quote round trip; landing faster shrinks the remaining window.

Where BoltTx Fits

We handle submission. Pool reading, pricing, and sizing all stay in your code.

Submissions route through our own delivery nodes in four regions with stake-weighted routing and no public mempool exposure, so a transaction is not observable in transit before it lands. Your reads keep whatever endpoint you already use — the paths are independent.

You sign locally. We never hold funds, never sign, and never modify transaction contents, including the minimum output you computed. The tip travels inside the transaction, paid on chain from your own wallet, and reverts with the transaction if it fails, because that is how Solana handles atomic transactions. You pay only on transactions that reach the chain.

Get a free API key. No monthly fee:

const sender = new Connection("https://la.bolttx.io/?api-key=YOUR_KEY");

FAQ

How do I read a Solana liquidity pool's reserves? Fetch the pool's two vault token accounts and read the amount at byte offset 64 in each. Use getMultipleAccountsInfo so both come from the same slot.

Why should I read reserves instead of using a quote API? No round trip in the hot path, a slot number you can check for staleness, the ability to verify a quote, and continued operation when the API is rate limited or does not know a new token.

How do I calculate the output of a constant-product swap? Apply the fee to the input, then multiply by the output reserve and divide by the input reserve plus the fee-adjusted input. That is the same arithmetic the program performs.

Where do I find the vault addresses for a pool? In the pool account's data, decoded using the program's IDL. Cache them, since they do not change for the lifetime of the pool.

Why must both reserves come from the same slot? Because reading them separately can straddle a slot boundary and give you a pair that never existed together, producing a price that was never real.

Which commitment should I use for reserve reads? processed when deciding, since the worst case for a rollback is a wasted attempt. Use a higher commitment only for values you record off chain.

Should I poll or subscribe to pool accounts? Subscribe. Pool accounts change on every trade, so polling either misses updates or wastes requests. onAccountChange pushes them as they happen.

Does reading reserves protect me from price movement? No. The pool can change between your read and your execution. Reserves tell you what to expect and how to size, while the on-chain minimum output protects you when reality diverges.

How do I calculate price impact? Compare your computed output against the output an infinitesimally small trade would receive. The difference is impact, and it is caused by your own size rather than by the market.

What does a zero reserve mean? Either the pool is uninitialised or it has been drained. It is common on new tokens and must be checked, since it produces either an error or a nonsensical price.

Do I still need a minimum output if I read reserves? Yes, always. Your computation is correct for the slot you read at, and the transaction executes later. The on-chain check is the only thing enforcing your expectation.

Are all Solana pools constant-product? No. Concentrated liquidity and stable-swap pools use different curves, so the arithmetic above applies to constant-product pools specifically. Check which type before assuming.

Should I stop using aggregators entirely? No. They add genuine value in route discovery across many venues. Reading reserves is better for a specific pool you have already chosen, not a replacement for finding the route.

How do I detect a pool that is too thin for my trade? Compute price impact for your intended size and reject above a threshold. The program will happily execute a trade that moves the price heavily against you.

How stale is a quote from an aggregator? Unknown, which is the problem. It does not report the slot it was computed at, so you cannot measure staleness. A direct read does, via the context slot.

What should I cache from a pool? The vault addresses, fee rate, and decimals, all of which are stable. Never cache the reserves themselves, since they change with every trade.

Back to all posts