How to Read Pump.fun Token Data On-Chain (Bonding Curve Account Layout)
Derive the bonding curve PDA, decode the account buffer, and read reserves, price, the creator/dev wallet, and migration status directly from Solana with web3.js — no Pump.fun API required.
Pump.fun’s HTTP API is convenient right up until it rate-limits, geo-blocks, or returns a 530 mid-launch. Everything it serves about a token’s curve lives on-chain and is readable with a single RPC call. This is the reliable way to do it.
The program and the account
Every Pump.fun token has a bonding curve account — a Program Derived Address (PDA) owned by the Pump.fun program. That account holds the curve state. You derive its address deterministically from the token mint, so given any contract address you can find its curve with no lookup table.
| Thing | Value |
|---|---|
| Pump.fun program ID | 6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P |
| PDA seeds | "bonding-curve" + mint pubkey bytes |
| Account owner | The Pump.fun program |
| Encoding | Anchor account — 8-byte discriminator, then fields |
Step 1 — Derive the bonding curve PDA
import { PublicKey } from "@solana/web3.js";
const PUMP_PROGRAM = new PublicKey(
"6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P"
);
function bondingCurvePda(mint: PublicKey): PublicKey {
const [pda] = PublicKey.findProgramAddressSync(
[Buffer.from("bonding-curve"), mint.toBuffer()],
PUMP_PROGRAM
);
return pda;
}Step 2 — Fetch and decode the account
Pull the raw account with getAccountInfo and decode the buffer. The layout after the 8-byte Anchor discriminator is five u64 reserve fields, a one-byte complete boolean, and then the 32-bytecreator public key.
Offset Size Field
────── ──── ─────────────────────────
0 8 discriminator (Anchor)
8 8 virtual_token_reserves (u64, LE)
16 8 virtual_sol_reserves (u64, LE)
24 8 real_token_reserves (u64, LE)
32 8 real_sol_reserves (u64, LE)
40 8 token_total_supply (u64, LE)
48 1 complete (bool)
49 32 creator (Pubkey) ← the dev walletimport { Connection, PublicKey } from "@solana/web3.js";
const connection = new Connection("https://api.mainnet-beta.solana.com");
async function readCurve(mintAddress: string) {
const mint = new PublicKey(mintAddress);
const pda = bondingCurvePda(mint);
const acct = await connection.getAccountInfo(pda);
if (!acct) return null; // no curve → not a pump.fun token
const b = acct.data;
return {
virtualTokenReserves: b.readBigUInt64LE(8),
virtualSolReserves: b.readBigUInt64LE(16),
realTokenReserves: b.readBigUInt64LE(24),
realSolReserves: b.readBigUInt64LE(32),
tokenTotalSupply: b.readBigUInt64LE(40),
complete: b.readUInt8(48) === 1,
creator: new PublicKey(b.subarray(49, 81)).toBase58(),
};
}Step 3 — Turn reserves into price and market cap
const c = await readCurve(mint);
const sol = Number(c.virtualSolReserves) / 1e9; // lamports → SOL
const toks = Number(c.virtualTokenReserves) / 1e6; // base → tokens (6 dp)
const priceSol = sol / toks; // SOL per token
const mcapUsd = priceSol * solPriceUsd * 1_000_000_000;
console.log({ priceSol, complete: c.complete, dev: c.creator });Reading the dev wallet — and verifying it
The creator field at offset 49 is the wallet that deployed the token. It is authoritative and tamper-proof — it is written by the program at creation. You can cross-check it against the oldest transaction on the bonding curve account (its fee payer will be the same wallet), which is a useful sanity check when you first wire this up.
Gotchas
- Public RPC is rate-limited.
api.mainnet-beta.solana.comis fine for testing; use Helius/QuickNode/Triton for anything production. - Migrated tokens still have the account. After graduation the
completeflag istruebut the account persists — always read the flag, don’t assume. - Endianness matters. All integers are little-endian. Use
readBigUInt64LE, not the big-endian variant. - Not every SPL token is a Pump.fun token. If
getAccountInfoon the derived PDA returns null, there is no curve — it wasn’t launched on Pump.fun.