Every Solana DEX Program ID and Pool Layout, Decoded
Verified program IDs and byte offsets for Raydium, Orca Whirlpool, Meteora, PumpSwap, Moonit and FluxBeam, from twelve decoders we run in production.
If you have a pool address and need to know which DEX it belongs to, what its account data contains, and how to turn that data into a price, this page is the whole answer. ScreenerBot decodes twelve Solana pool programs locally, on every price tick, so every constant below is one we run in production rather than one copied from a blog post.
Start with the identification table. Everything after it is detail.
The program IDs
A pool account's owner is the DEX program. One getAccountInfo call tells you which decoder to use — you never have to guess from the token pair or the pool name.
| DEX / program | Program ID | Pricing model |
|---|---|---|
| Raydium AMM v4 (legacy) | 675kPX9MHTjS2zt1qfr1NYHuzeLXfQM9H24wFSUt1Mp8 | vault reserves |
| Raydium CPMM | CPMMoo8L3F4NbTegBCKVNunggL7H1ZpdTHKxQB5qKP1C | vault reserves |
| Raydium CLMM | CAMMCzo5YL8w4VFF8KVHrK22GGUsp5VTaW7grrKgrWqK | sqrt_price Q64.64 |
| Orca Whirlpool | whirLbMiicVdio4qvUfM5KAg6Ct8VwpYzGff3uctyCc | sqrt_price Q64.64 |
| Orca v1 (legacy) | 9W959DqEETiGZocYWCQPaJ6sBmUzgfxXfqGeTEdp3aQP | vault reserves |
| Meteora DLMM | LBUZKhRxPF3XUpBCjp4YzTKgLccjZhTSDM9YuVaPwxo | bin reserves |
| Meteora DAMM | cpamdpZCGKUy5JxQXB4dcpGPiikHawvSWAd6mEn1sGG | sqrt_price Q64.64 |
| Meteora DBC (bonding curve) | dbcij3LWUppWqq96dh6gJWwBifmcGfLSB5D4DuSMaqN | curve reserves |
| PumpSwap (pump.fun AMM) | pAMMBay6oceH9fJKBRHGP5D4bD4sWpmSwMn52FMfXEA | vault reserves |
| pump.fun (bonding curve) | 6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P | virtual reserves |
| Moonit (Moonshot) | MoonCVVNZFSYkqNXP6bxHLPL6QQJiMagDL3qcqUQTrG | curve reserves |
| FluxBeam | FLUXubRmkEi2q6K3Y9kBPg9248ggaZVsoSFhtJHSrm1X | vault reserves |
Routers and aggregators are not pool programs. If a transaction's top-level program is one of these, the pool it touched is still one of the above:
| Router | Program ID |
|---|---|
| Jupiter v6 | JUP6LkbZbjS1jKKwapdHNy74zcZ3tLUZoi5QNyVTaV4 |
| Jupiter v4 | JUP4Fb2cqiRUcaTHdrPC8h2gNsA2ETXiPDD33WcGuJB |
| Jupiter v3 | JUP3c2Uh3WA4Ng34tw6kPd2G4C5BB21Xo36Je1s32Ph |
| GMGN | GMGNjvGr7ddxt2u1XSf8Zo6LLnDjDm9mJahGfhq7j6gk |
There are only two pricing models
Twelve programs, but only two ways to get a price. Knowing which one a pool uses tells you what to fetch.
Reserve-based (constant product). The pool owns two SPL token accounts. The price is the ratio of their balances, adjusted for decimals. You must fetch the two vault accounts in addition to the pool account.
Square-root price (concentrated liquidity). The pool account itself stores sqrt_price as a Q64.64 fixed-point number. No vault fetch is needed for a price — one account read is enough, which makes CLMM-style pools materially cheaper to poll.
Reading reserves from a vault
Every vault is a standard SPL token account, so one rule covers all of them: the amount is a little-endian u64 at byte offset 64, and the mint is the pubkey at offset 0. That second field is worth reading even when you think you know the answer — it is how you catch a mis-parsed vault before it becomes a wrong price.
fn token_account_amount(data: &[u8]) -> Option<u64> {
if data.len() < 72 {
return None;
}
Some(u64::from_le_bytes(data[64..72].try_into().ok()?))
}
Price from reserves, with decimals applied:
price_in_sol = (sol_reserve / 10^sol_decimals) / (token_reserve / 10^token_decimals)
Converting a Q64.64 square-root price
sqrt_price encodes the square root of the price of token B per token A, in Q64.64 fixed point. Square it, divide by 2^128, then correct for the decimal difference:
price_b_per_a = (sqrt_price^2 / 2^128) * 10^(decimals_a - decimals_b)
Doing it as (sqrt_price / 2^64)^2 first, rather than sqrt_price^2 / 2^128, keeps the intermediate value inside f64 range. Squaring a raw u128 before dividing overflows for high-priced pools, and the failure is silent.
Raydium AMM v4 (legacy)
The long-lived Raydium AMM. The account is LIQUIDITY_STATE_LAYOUT_V4: 32 consecutive u64 fields (bytes 0–256), then a block of swap counters, then the pubkeys. The offsets that matter:
| Field | Offset | Size |
|---|---|---|
quoteTotalPnl | 208 | 8 |
baseTotalPnl | 216 | 8 |
baseVault | 336 | 32 |
quoteVault | 368 | 32 |
baseMint | 400 | 32 |
quoteMint | 432 | 32 |
lpMint | 464 | 32 |
Read the two mints, decide which side is SOL, then fetch the matching vault and read its balance at offset 64.
Verify each vault's mint against the mint you read from the pool account before you trust its balance. A hardcoded vault offset that is even a few bytes off still yields a syntactically valid pubkey — it just points at a different account, and you get a confident, wrong price rather than an error.
Raydium CPMM
The modern constant-product program, and the one with Token-2022 support. The account is Anchor-style: an 8-byte discriminator, then fields in a fixed order with no padding until the tail.
| Field | Offset |
|---|---|
| discriminator | 0 |
amm_config | 8 |
pool_creator | 40 |
token_0_vault | 72 |
token_1_vault | 104 |
lp_mint | 136 |
token_0_mint | 168 |
token_1_mint | 200 |
token_0_program | 232 |
token_1_program | 264 |
observation_key | 296 |
auth_bump, status, lp_mint_decimals, pool_mint_0_decimals, pool_mint_1_decimals | 328–333 |
lp_supply | 333 |
token_0_program and token_1_program are the reason this layout matters: a CPMM pool can hold a Token-2022 mint on either side, and a transfer-fee extension on that mint means the amount that reaches the pool is smaller than the amount you sent. Read those two fields rather than assuming the classic SPL Token program.
Raydium CLMM and Orca Whirlpool
Both are concentrated liquidity, both price from sqrt_price, and both are single-account reads. Orca's Whirlpool account is the better documented of the two, and its layout is stable:
| Field | Offset | Size |
|---|---|---|
| discriminator | 0 | 8 |
whirlpools_config | 8 | 32 |
whirlpool_bump | 40 | 1 |
tick_spacing | 41 | 2 |
fee_tier_index_seed | 43 | 2 |
fee_rate | 45 | 2 |
protocol_fee_rate | 47 | 2 |
liquidity | 49 | 16 |
sqrt_price | 65 | 16 |
tick_current_index | 81 | 4 |
protocol_fee_owed_a | 85 | 8 |
protocol_fee_owed_b | 93 | 8 |
token_mint_a | 101 | 32 |
token_vault_a | 133 | 32 |
fee_growth_global_a | 165 | 16 |
token_mint_b | 181 | 32 |
token_vault_b | 213 | 32 |
A valid Whirlpool account is at least 653 bytes; treat anything shorter as a decode failure rather than parsing it. Note that liquidity and sqrt_price are u128, not u64 — reading either as eight bytes gives a plausible-looking number that is wrong by an enormous factor.
fee_tier_index_seed at offset 43 is a comparatively recent addition. If you are working from an older struct definition that omits it, every field after offset 43 shifts by two bytes and the whole tail decodes to garbage. This is the most common cause of "my Whirlpool decoder used to work".
Meteora: DLMM, DAMM and DBC
Meteora ships three distinct programs and they do not share a layout.
DLMM is a discretized liquidity market maker: liquidity sits in fixed-width bins, and the pool stores an active_id and a bin_step rather than a single price. The theoretical price of the active bin is:
price = (1 + bin_step / 10000) ^ active_id
That value is the bin's nominal price and it ignores decimals and the real balance of the active bin. For anything that touches money, read the actual reserve_x and reserve_y token accounts referenced by the pool and price from those balances. Use the bin math as a sanity check on the reserve-derived price, not as a replacement for it.
DAMM is a constant-product pool that also carries a sqrt_price. In practice the two disagree on very low-liquidity pools, where accumulated protocol fees are large relative to the reserves. Our decoder cross-checks the square-root price against the vault ratio and rejects the square-root value when it diverges by more than a few hundred percent. If you only implement one path, implement the vault ratio.
DBC is Meteora's bonding-curve launch program. A DBC pool that has not graduated has no conventional pair of vaults — check both vault fields against the system program address 11111111111111111111111111111111 and treat that as "not yet a real pool" rather than as a zero balance.
pump.fun and PumpSwap
These are two different programs at two different stages of a token's life, and conflating them is a common source of missing pools.
pump.fun (6EF8rrec...) is the bonding curve. A new token trades against a virtual-reserve curve on this program, with no LP and no external liquidity.
PumpSwap (pAMMBay6...) is the AMM a token graduates into. Its pool account is Anchor-style, and the field order is: discriminator (8), pool_bump (1), index (2), then the pubkeys — creator, base_mint, quote_mint, lp_mint, pool_base_token_account, pool_quote_token_account — followed by lp_supply.
Because a coin_creator field was added to this struct after launch, the offset of lp_supply differs between older and newer pools. Derive it by walking the pubkeys forward from offset 11 rather than hardcoding it, and validate that the account owner really is pAMMBay6... before parsing — the discriminator alone is not a strong enough check.
Fetching efficiently
Decoding is cheap; fetching is not. Three rules keep an RPC budget under control:
- Batch pool and vault reads with
getMultipleAccountsinstead of issuing onegetAccountInfoper account. Keep each batch to 50 accounts or fewer — that stays inside the limits of every provider we have used, including those that advertise 100. - Prefer concentrated-liquidity pools when you only need a price. One account read beats three.
- Cache the pool-account decode. Mints, vaults and decimals never change for a given pool; only the balances and
sqrt_pricedo. Re-reading the layout on every tick wastes the majority of your request budget.
Avoid getProgramAccounts against these programs in a hot path. It is heavily rate-limited or disabled on most providers, and on a program the size of Raydium AMM v4 the response is enormous.
The traps, in order of how much they cost
- Assuming base is the token. Always compare both mints against wrapped SOL,
So11111111111111111111111111111111111111112, and orient from that. Getting this wrong inverts the price. - Reading a
u128as au64.liquidityandsqrt_priceare 16 bytes in every concentrated-liquidity layout here. - Squaring
sqrt_pricebefore dividing. Divide by2^64first, then square. - Trusting a vault offset without checking its mint. A wrong offset yields a valid-looking pubkey and a confidently wrong price, never an error.
- Ignoring
token_0_programandtoken_1_programon CPMM. A Token-2022 transfer fee changes the amount that actually arrives. - Forgetting decimals. Every formula here needs
10^(decimals_a - decimals_b); skipping it produces prices off by orders of magnitude.
Why decode at all
A price API returns a number somebody else computed, at their cadence, with their idea of which pool matters. Decoding the pool yourself gives you the price at your own cadence, from the specific pool you are going to trade against, with no third party between your decision and the chain. ScreenerBot refreshes pool prices roughly twice a second from local decoders for exactly that reason — and the same twelve program IDs above are what make it possible.
If you would rather read this data than build it, the ScreenerBot Terminal shows live pool-derived prices for Solana markets, and Token DNA traces the on-chain history behind them. If you want the pipeline running on your own machine, the desktop app does all of the above locally.
For the API layer that sits above these programs, see our Jupiter API guide, and for the maths behind constant-product pricing, how DEX prices work.