Solana PDAs Explained: Addresses With No Private Key

Look up any token you hold in an explorer and follow it to the account that actually stores your balance. That address isn't your wallet, and you never generated a key for it. Nobody did. No private key exists for it at all, and that's on purpose. It's a program derived address, a PDA, and once you get how Solana PDAs work, a lot of the network stops looking like magic.
This one's for builders who want the mechanics and for traders who keep seeing "PDA" in explorers and audits and want to know what it actually promises. I'll cover what a PDA is, how the address gets derived, where you already use one every day, and the part people get wrong: "no private key" does not mean "nobody's in control." Everything here comes from Solana's own docs, linked as I go.
What a program derived address is
Normal Solana addresses come from keypairs. Per Solana's PDA derivation docs, a keypair's public key is a point on the Ed25519 curve, and whoever holds the matching secret key can sign for that address. That's your wallet.
A PDA is built to land off that curve. The PDA overview describes them as 32-byte addresses derived deterministically from a program ID and a set of seeds, and checked so they never land on the curve, so no private key exists for them. Only the program whose ID went into the derivation can "sign" for a PDA, and it does that through invoke_signed when it calls another program.
That gives you two things a keypair can't:
- A predictable address. The same seeds and program ID always produce the same PDA, so anyone can recompute where an account lives instead of storing or looking it up.
- An authority that's code, not a person. A program can own a vault or a config account and move things out of it under its own rules, with no human key sitting in a drawer somewhere.
How a Solana PDA gets derived
The recipe has three ingredients: optional seeds (byte strings you choose, like the word "vault" or a user's wallet address), the program ID, and a one-byte "bump." Solana's docs lay out the steps:
- Hash the seeds, the program ID and the string
"ProgramDerivedAddress"together with SHA-256. - Check whether the 32-byte result is a valid point on the Ed25519 curve.
- If it is on the curve, reject it, because that address could have a private key.
- If it isn't, that's your PDA.
The bump is what makes step 3 survivable. find_program_address appends a bump to the seeds, starting at 255 and counting down, until the hash falls off the curve. The first one that works is the canonical bump. The docs' own example uses the seed "helloWorld": bump 255 lands on the curve and fails, bump 254 works, so 254 is canonical for those seeds.
One more thing that trips people up: deriving a PDA only computes an address. It doesn't create anything on-chain. The account still has to be created by a separate instruction, usually a call into the System Program, and it needs a rent-exempt deposit like any other account.
You already use PDAs every day
That token account from the opening? If it's a standard one, it's an associated token account, and Solana's token account docs show it's derived with find_program_address from three seeds: your wallet address, the token program address and the mint address, under the Associated Token Program. For any wallet, token program and mint combination, there's exactly one ATA address.
That's why a wallet can show you a token you never "added." It just recomputes the address and checks it. It's also why sending to someone's wallet address works: the app derives their token account for that mint and, if it doesn't exist yet, creates it (somebody pays the storage deposit, which I broke down in where your leftover SOL goes with token accounts and rent). Note the token program is one of the seeds, so the original Token Program and Token-2022 give the same wallet and mint different ATA addresses.
The same pattern runs everywhere programs keep state. The docs list common seed layouts: a single ["global"] config account, a per-user account from ["user", user_pubkey], and per-user-per-token vaults from something like ["vault", user_pubkey, mint_pubkey].
No private key doesn't mean nobody's in control
This is the part worth slowing down on, especially if you're judging a project and not writing one. "Funds sit in a PDA, no one has the key" sounds like a safety promise. It isn't. It moves the question from "who holds the key?" to "what does the program let happen, and who can change the program?"
A PDA's signer is its program. Whatever the program's code allows, the PDA will do, including sending everything in a vault to an address the code accepts. And Solana's programs docs note that programs deployed with the upgradeable loader can be upgraded while an upgrade authority is set, and revoking that authority makes the program immutable. So if a program is still upgradeable, whoever holds that authority can change the rules every one of its PDAs follows.
That doesn't make upgradeable programs bad. Plenty of teams keep upgrades on so they can patch bugs, and that's a real tradeoff, not a scam signal by itself. It just means "it's a PDA" is where the trust question starts, not where it ends. The questions that matter are what the program is allowed to do with that account and who can rewrite it. If you want to see which accounts and programs a specific transaction touches, my guide on how to read a Solana transaction walks through it.
Gotchas if you're building with PDAs
- Always use the canonical bump. Solana's docs warn that a non-canonical bump creates a second valid address for the same seeds, which can let an attacker substitute a different account than the one you expected. Validate it, don't take whatever bump a client sends.
- Seeds are concatenated before hashing. Per the docs,
["ab", "cd"]and["abcd"]give the same PDA. Use fixed-length seeds or a separator so two different meanings can't collide. - Mind the limits. Up to 16 seeds, 32 bytes max per seed. A pubkey is exactly 32 bytes, so it fits; a long string might not.
- Searching costs compute. The docs list 1,500 compute units for
create_program_address, and the find syscall charges 1,500 on entry plus 1,500 for each failed bump. Store the canonical bump in the account once and reuse it rather than searching on every instruction. If compute units are new to you, see Solana priority fees and compute units, explained. - Derivation isn't creation. Check the account actually exists and is owned by the program you expect before you read from it.
What this doesn't cover
I'm sticking to what Solana's core docs say about PDAs. I haven't audited any specific program, and I'm not claiming any particular app's vaults are safe or unsafe because they use PDAs. Framework helpers like Anchor's seeds and bump constraints handle a lot of this for you, but they're their own topic. Token-level powers, like mint and freeze authority or Token-2022 extensions, are a separate check I covered in how to spot suspicious activity in new token launches.
The takeaway
A Solana PDA is an address computed from seeds and a program ID, pushed off the Ed25519 curve with a bump so no private key can exist for it. That's what lets programs own accounts and sign for them, and it's why your token accounts show up at predictable addresses. But the program is the key now. When someone tells you funds are "safe in a PDA," the next question is always what that program can do and who can still upgrade it.
More case files: Solana commitment levels explained, and the live crypto news page.
Still building. Still here. 🫡
Keep reading: all articles · DarkGeneral Labs · live crypto news · latest posts on X.


