Solana PDA Derivation and the Bump You Should Cache

How findProgramAddressSync works, why deriving in the hot path costs you, and the difference between canonical and arbitrary bumps.

BoltTx Team··9 min read
solanapdaprogram-derived-addressseedstransaction-landingrpc

A program derived address is an address with no private key, owned by a program instead of a person. Every pool account, every vault, every per-user state account you interact with is one.

Deriving them is simple enough that most guides stop at the one-liner. ★The parts that matter for a bot are the ones that come after: what it costs, when to do it, and which bump you are actually using.★

The Derivation

import { PublicKey } from "@solana/web3.js";

const [pda, bump] = PublicKey.findProgramAddressSync(
  [
    Buffer.from("vault"),          // literal seed
    owner.toBuffer(),              // ★32 bytes★
    mint.toBuffer(),               // ★32 bytes★
  ],
  PROGRAM_ID,
);

The function hashes your seeds with the program ID and checks whether the result falls off the ed25519 curve. If it lands on the curve, an address with a private key could exist, so that result is unusable. It then decrements a bump byte from 255 and tries again until it finds one that is off-curve.

★That loop is the cost.★ It is fast in the common case and unbounded in principle, and it runs every single time you call it.

Why You Cache It

The seeds for a given account never change, which means the derived address never changes:

// ★Same inputs, same output, every time. Derive once.★
const pdaCache = new Map<string, [PublicKey, number]>();

function getVault(owner: PublicKey, mint: PublicKey) {
  const key = `${owner.toBase58()}:${mint.toBase58()}`;
  let v = pdaCache.get(key);
  if (!v) {
    v = PublicKey.findProgramAddressSync(
      [Buffer.from("vault"), owner.toBuffer(), mint.toBuffer()],
      PROGRAM_ID,
    );
    pdaCache.set(key, v);
  }
  return v;
}

For a bot deriving a handful of PDAs per transaction, at high frequency, this is real CPU that you are spending repeatedly on an answer that cannot change. ★For a launch snipe, it belongs in the precompute phase alongside the blockhash and the connection warm-up — not between detecting the launch and calling sendRawTransaction.★

If your derivation is precomputed and transactions still land late, a free BoltTx key is one line to test the submission path.

The Bump Is Not Decoration

findProgramAddressSync returns the canonical bump — the highest value that produces a valid off-curve address. That distinction matters because other bumps can also produce valid addresses.

// ★Canonical: the first one that works, counting down from 255.★
const [pda, bump] = PublicKey.findProgramAddressSync(seeds, PROGRAM_ID);

// ★Arbitrary: you supply the bump, no search happens.★
const alt = PublicKey.createProgramAddressSync(
  [...seeds, Buffer.from([bump - 1])],
  PROGRAM_ID,
);

★If a program accepts a user-supplied bump without checking it is canonical, two different valid addresses can exist for the same logical account.★ That is a real class of vulnerability — the program thinks it is operating on one account and the caller has pointed it at another.

As a caller, the practical rule is simple: always pass the canonical bump. Programs that store their bump on chain expect it, and programs that recompute it will disagree with you if you pass anything else.

Store the Bump, Do Not Re-Derive On Chain

This is where the cost shows up inside the program, and it is a common review finding in Anchor code:

// ★Expensive: runs the search loop on chain, every call.★
let (pda, bump) = Pubkey::find_program_address(seeds, program_id);

// ★Cheap: verifies one candidate against a stored bump.★
let pda = Pubkey::create_program_address(
    &[seed_a, seed_b, &[stored_bump]],
    program_id,
)?;

find_program_address runs the same descending search on chain, consuming compute units for a result the program could have stored when the account was created. ★A program deriving several PDAs per instruction this way can spend a meaningful share of its compute budget on work that is already known.★

In Anchor this is the difference between letting the framework find the bump and declaring bump = account.bump against a stored value.

Seed Rules That Bite

32 bytes per seed, 16 seeds maximum. A public key is exactly 32 and fits; a longer string does not.

// ★Too long — this throws.★
Buffer.from("a_very_long_seed_string_that_exceeds_the_limit_here")

// ★Hash it down to 32.★
createHash("sha256").update(longValue).digest()

Seeds are bytes, not strings. Buffer.from("vault") and Buffer.from("Vault") derive different addresses, and so do a u64 written little-endian versus big-endian. ★A mismatch here produces an address that simply does not exist on chain, and the error you get is AccountNotFound — which points at the account rather than at your encoding.★

// Match the program's byte order exactly.
const indexSeed = Buffer.alloc(8);
indexSeed.writeBigUInt64LE(BigInt(index));   // ★LE, matching Rust's to_le_bytes★

Order is part of the identity. Swapping two seeds gives a different address, not an error.

Debugging a PDA That Does Not Match

When a derived address is not what the program expects, work through it in this order:

const [pda, bump] = PublicKey.findProgramAddressSync(seeds, PROGRAM_ID);
console.log("derived:", pda.toBase58(), "bump:", bump);

const info = await connection.getAccountInfo(pda);
console.log("exists:", !!info, "owner:", info?.owner.toBase58());

owner is the single most useful field here.★ If the account exists but is owned by a different program, your seeds are right and your program ID is wrong. If it does not exist at all, either the seeds are wrong or the account has genuinely not been created yet — and for a new token, the second is the normal case.

This is why launch sniping cannot precompute PDAs for the token itself. The mint does not exist before the launch, so any PDA seeded with it cannot be derived in advance. Precompute everything seeded with values you already know, and accept that the rest is on the critical path.

What Landing Looks Like

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

★Derivation is microseconds; landing is slots.★ Caching PDAs is worth doing because it is free, not because it changes which block you land in — that part is decided after your bytes leave the process.

Where BoltTx Fits

We handle submission. Address derivation happens entirely in your process before anything is signed.

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.

You sign locally. We never hold funds, never sign, and never modify transaction contents — including the accounts your instructions reference. 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 connection = new Connection("https://la.bolttx.io/?api-key=YOUR_KEY");

FAQ

What is a PDA on Solana? A program derived address — an address deliberately off the ed25519 curve, so no private key exists for it. Programs sign for these addresses instead, which is how they own accounts.

How do I derive a PDA in JavaScript? PublicKey.findProgramAddressSync(seeds, programId) returns the address and its canonical bump. The seeds and their order must match exactly what the program uses.

What is the bump in a PDA? A byte appended to the seeds so the result falls off the curve. findProgramAddressSync counts down from 255 and returns the first value that works, which is the canonical bump.

Should I cache PDA derivations? Yes. The same seeds always produce the same address, and the derivation runs a search loop each time. For latency-sensitive code it belongs in a precompute phase, not the hot path.

Why is find_program_address expensive on chain? It runs the same descending search inside the program, consuming compute units for a result you could have stored. Use create_program_address with a stored bump instead.

What is the difference between canonical and non-canonical bumps? The canonical bump is the highest value producing a valid address. Other bumps can also produce valid addresses, so a program accepting an unchecked user-supplied bump may operate on an unintended account.

What is the maximum seed length? 32 bytes per seed, with at most 16 seeds. A public key fits exactly. Longer values need hashing down to 32 bytes first.

Why does my derived PDA not match the program's? Usually a seed encoding mismatch — a different string case, or a number written in the wrong byte order. Rust's to_le_bytes corresponds to writeBigUInt64LE, not the big-endian variant.

Why do I get AccountNotFound for a PDA? Either the seeds are wrong so you derived an address nobody created, or the account genuinely does not exist yet. Check getAccountInfo and look at the owner field to tell them apart.

Can I derive a PDA for a token that does not exist yet? No, if the mint is one of the seeds. This is why launch snipers cannot precompute those addresses and must derive them after the mint appears.

Does seed order matter? Yes. Different order means a different address, with no error to tell you. The order is part of the account's identity as far as the program is concerned.

How do I pass a number as a seed? Write it to a buffer in the byte order the program uses, usually little-endian for Rust. Buffer.alloc(8) with writeBigUInt64LE matches a u64 written with to_le_bytes.

Are associated token accounts PDAs? Yes. An ATA is derived from the wallet, the token program, and the mint, which is why getAssociatedTokenAddress is a derivation rather than a lookup.

Can two different seed sets produce the same PDA? Practically no, since the seeds are hashed together with the program ID. The realistic collision risk is non-canonical bumps producing a second valid address for the same logical account.

Do PDAs need rent exemption? Yes, like any account holding data. The program pays it from whoever funds the creating transaction, and it is recoverable if the account is later closed.

How many PDAs can one transaction reference? The same limit as any account: 32 bytes each against the 1232-byte transaction size. PDAs are ordinary account keys once derived, so lookup tables compress them the same way.

Back to all posts