Free guide

Program Derived Addresses, explained

An address computed from seeds instead of minted from a keypair, guaranteed to have no private key — and how a program signs for it anyway.

An address with no private key

Every keypair-backed account on Solana sits at a point on the ed25519 curve, with a private key somewhere in the world that can sign for it. A Program Derived Address is the opposite by construction. It is computed from two inputs: a program's ID and a list of seeds, arbitrary bytes the program chooses. A string label like b"page_visits" plus a user's public key is the classic pair. The derivation deliberately searches for a result that falls off the curve, where no private key can exist. The search counter, a single byte called the bump, becomes part of the recipe: reproducing the address later means supplying the same seeds, the same program ID, and the same bump.

What determinism buys you

Two consequences fall out of that derivation, and every PDA pattern in the ecosystem is one of them wearing different clothes.

The recipe, in Anchor

The official program-derived-addresses example keeps one page-visit counter per user. Its state file is small enough to read whole, and it establishes the first habit worth copying: the seed label lives in one constant, not repeated as a string literal in every instruction.

// state/page_visits.rs
use anchor_lang::prelude::*;

#[account]
#[derive(InitSpace)] // automatically calculate the space required for the struct
pub struct PageVisits {
    pub page_visits: u32,
    pub bump: u8,
}

impl PageVisits {
    pub const SEED_PREFIX: &'static [u8; 11] = b"page_visits";

    pub fn increment(&mut self) {
        self.page_visits = self.page_visits.checked_add(1).unwrap();
    }
}

Creating a user's counter is the standard Anchor init, with two additions that turn a fresh keypair address into a derived one:

// instructions/create.rs
#[derive(Accounts)]
pub struct CreatePageVisits<'info> {
    #[account(mut)]
    payer: Signer<'info>,

    #[account(
        init,
        space = 8 + PageVisits::INIT_SPACE,
        payer = payer,
        seeds = [
            PageVisits::SEED_PREFIX,
            payer.key().as_ref(),
        ],
        bump,
    )]
    page_visits: Account<'info, PageVisits>,
    system_program: Program<'info, System>,
}

From solana-foundation/program-examples, MIT licensed, © Solana Foundation, pinned to commit 9389865.

seeds = [PageVisits::SEED_PREFIX, payer.key().as_ref()] is the address recipe: the constant label plus this specific payer's public key. Anchor derives the address, confirms nothing already lives there, creates the account, and hands the bump it found to the handler in ctx.bumps. The example stores that bump in the account, the second habit worth copying: later instructions supply bump = page_visits.bump and skip the search entirely.

The client already knows the address

The clearest demonstration of a PDA's core property is in the example's test suite, where the address is derived once, at the top of the file, before any instruction has run:

const [pageVisitPDA] = PublicKey.findProgramAddressSync(
  [Buffer.from("page_visits"), payer.publicKey.toBuffer()],
  program.programId,
);

Every subsequent call, the create and each increment, reuses that precomputed address. The client never asks the program what it created. It already knew, because the derivation is public and deterministic. When your frontend needs a user's account, this one call replaces a round trip, a lookup table, and a registry that would otherwise have to exist.

Where PDAs go from here

A per-user counter is the teaching case. The same derivation, with different seeds, is how an escrow holds both sides of a swap, how an AMM pool owns its token vaults, and how Token-2022 attaches metadata to a mint. The seeds change; the two properties never do.