Good to see you again. In the previous lesson, you derived PDAs deterministically from a seed schema, a program ID, and a canonical bump. The key boundary was important: derivation calculates an address but creates no on-chain storage.
Now you will cross that boundary. You will use Anchor constraints to create a program-owned data account at a PDA, charge a signer for its storage allocation, and record the PDA’s canonical bump in its state. This is the standard foundation for profiles, escrow records, configuration state, token vault authorities, and other program-controlled objects.
From deterministic address to allocated state
For a per-user profile, retain the seed schema from the previous lesson:
["profile", user's public key bytes]
This schema determines one expected PDA per user for your program. But until someone submits an initialization transaction, RPC will report no account at that address.
Initializing an Anchor PDA account combines five responsibilities:
| Constraint or component | Responsibility |
|---|---|
init | Creates the account rather than merely validating an existing one. |
seeds | Declares the logical byte seeds that identify the PDA. |
bump | Uses and validates the canonical bump for those seeds. |
payer | Names the writable signing account that funds the account’s initial lamports. |
space | Allocates the number of bytes needed for the account’s serialized data. |
system_program | Supplies the System Program Anchor invokes to create and fund the account. |
The account becomes owned by the executing Anchor program, not by the user who paid for it. The payer supplies the SOL required for allocation; ownership determines which program is allowed to modify its data.
At a lower level, creation requires the new account to authorize its creation. A normal new account would sign with a keypair. A PDA has no private key, so Anchor performs the required System Program CPI using the PDA’s seeds. The runtime verifies that those seeds derive a PDA for the program currently executing. This is why another program cannot initialize an account at a PDA in your program’s namespace.
Read the official Solana documentation now; it connects this runtime model to the exact Anchor implementation.
Read Solana’s official “PDA Accounts” guide to see the distinction between deriving a PDA and creating an account there, followed by a complete Anchor program and TypeScript invocation.
In “Create a PDA Account,” begin with the creation model. Then read the full Program code in the next section and its explanation of init, seeds, bump, payer, and space. Finally, in the Test block, follow the client’s PDA derivation, initialization call, and subsequent account fetch. Notice that repeating the same initialization fails because the deterministic PDA account already exists.
A minimal, production-shaped profile account
Here is a compact Anchor program that creates one Profile account per wallet. It stores the wallet that initialized the profile and the canonical bump.
use anchor_lang::prelude::*;
declare_id!("ReplaceThisWithYourProgramId");
#[program]
pub mod pda_profile {
use super::*;
pub fn initialize_profile(ctx: Context<InitializeProfile>) -> Result<()> {
let profile = &mut ctx.accounts.profile;
profile.authority = ctx.accounts.authority.key();
profile.bump = ctx.bumps.profile;
Ok(())
}
}
#[derive(Accounts)]
pub struct InitializeProfile<'info> {
#[account(mut)]
pub authority: Signer<'info>,
#[account(
init,
payer = authority,
space = 8 + Profile::INIT_SPACE,
seeds = [b"profile", authority.key().as_ref()],
bump,
)]
pub profile: Account<'info, Profile>,
pub system_program: Program<'info, System>,
}
#[account]
#[derive(InitSpace)]
pub struct Profile {
pub authority: Pubkey,
pub bump: u8,
}
Read it in two layers: first the state layout, then the account constraints.
The state layout and allocated space
Profile is the serialized payload that will live in the PDA account:
#[account]
#[derive(InitSpace)]
pub struct Profile {
pub authority: Pubkey,
pub bump: u8,
}
#[account] marks this as an Anchor account type. Anchor serializes and deserializes it and prefixes its stored data with an 8-byte account discriminator. The discriminator lets Anchor distinguish Profile data from another account type that happens to have a similar byte layout.
The state fields consume:
| Stored item | Bytes |
|---|---|
authority: Pubkey | 32 |
bump: u8 | 1 |
Profile::INIT_SPACE | 33 |
| Anchor discriminator | 8 |
| Total allocation | 41 |
That is the meaning of:
space = 8 + Profile::INIT_SPACE
#[derive(InitSpace)] calculates the 33 bytes for the fields, while you explicitly add the 8-byte Anchor discriminator.
For fixed-size fields, this is pleasantly mechanical. Variable-length fields require an explicit maximum allocation. For example, a future String field needs bytes for its 4-byte length prefix plus the declared maximum byte length; Anchor’s #[max_len(...)] annotation can make that capacity visible to InitSpace. Never allocate based on what a particular input happens to contain today: account size is chosen when it is created and cannot silently grow during ordinary serialization.
Reading the constraints as an execution contract
The central declaration is this account field:
#[account(
init,
payer = authority,
space = 8 + Profile::INIT_SPACE,
seeds = [b"profile", authority.key().as_ref()],
bump,
)]
pub profile: Account<'info, Profile>,
Anchor validates and prepares accounts before your initialize_profile handler runs. Each line contributes a distinct guarantee.
seeds and bump: require the exact PDA
seeds = [b"profile", authority.key().as_ref()],
bump,
These are the on-chain form of the seed protocol you implemented on the TypeScript side previously:
["profile", authority public key bytes]
The static b"profile" prefix creates a namespace. authority.key().as_ref() contributes the raw 32 bytes of the signer’s public key. The bare bump instructs Anchor to find and use the canonical bump.
The client cannot substitute an arbitrary writable account as profile. Anchor derives the expected address during validation, compares it with the supplied account, and rejects the instruction if they differ.
The image below shows the same general arrangement in an Anchor context: a named account field is decorated with init, a payer, a space allocation, and PDA constraints.

init: create, fund, and assign ownership
init means that profile must not already exist as an initialized account at that address. Anchor orchestrates the System Program account-creation CPI, allocates space bytes, sets this program as owner, and makes the account available to the instruction handler as Account<'info, Profile>.
The creation is part of the transaction’s atomic execution. If account validation or the handler later fails, the whole transaction fails rather than leaving a partially initialized profile behind.
This is also why a second call with the same authority normally fails. Identical seeds produce the same PDA, and that account is already present. This deterministic uniqueness is useful: one wallet cannot accidentally acquire two canonical profiles under this seed schema.
payer = authority: who funds the account
The payer must be both writable and a signer:
#[account(mut)]
pub authority: Signer<'info>,
It is a signer because the transaction must authorize spending its SOL. It is mutable because its lamport balance decreases when funding the new account’s allocation.
The account’s initial lamports are the amount needed to make its allocated storage rent-exempt under the network’s current rules. This is a storage deposit held in the account’s balance, not a fee paid to your program. A later close operation can return those lamports to a chosen destination; account closing is a separate lifecycle operation.
system_program: the program that creates accounts
pub system_program: Program<'info, System>,
Anchor needs the System Program because normal account allocation and ownership assignment are System Program operations. Giving it the strongly typed Program<'info, System> wrapper verifies that the supplied account really is Solana’s System Program rather than a lookalike account.
Initialize state after Anchor has created the account
Once validation and initialization finish, the handler receives a mutable, typed Profile:
pub fn initialize_profile(ctx: Context<InitializeProfile>) -> Result<()> {
let profile = &mut ctx.accounts.profile;
profile.authority = ctx.accounts.authority.key();
profile.bump = ctx.bumps.profile;
Ok(())
}
Two lines establish useful invariants:
profile.authorityrecords whose profile this is.profile.bumppersists the canonical bump Anchor used.
Although Anchor can derive the canonical bump again from the seed schema, storing it is practical. Later, when the program must sign a CPI as this PDA, it can reconstruct the exact signer-seed array without repeating a bump search. The next lesson will make that use concrete.
Do not mistake storing authority for enforcing future authorization. This initialization instruction proves that the profile address was derived from authority, but a future update_profile instruction still needs a constraint or explicit check ensuring its signer is the recorded authority. PDA derivation gives you a deterministic identity; authorization policy must still be written deliberately.
Invoke the instruction from an Anchor TypeScript client
The client derives the PDA with the same byte-level schema:
import * as anchor from "@coral-xyz/anchor";
import { PublicKey, SystemProgram } from "@solana/web3.js";
const provider = anchor.AnchorProvider.env();
anchor.setProvider(provider);
// Assume `program` is your generated, typed Anchor Program client.
const authority = provider.wallet.publicKey;
const [profilePda, profileBump] = PublicKey.findProgramAddressSync(
[
Buffer.from("profile", "utf8"),
authority.toBuffer(),
],
program.programId,
);
console.log("Profile PDA:", profilePda.toBase58());
console.log("Canonical bump:", profileBump);
Then submit the initialization transaction:
const signature = await program.methods
.initializeProfile()
.accounts({
authority,
profile: profilePda,
systemProgram: SystemProgram.programId,
})
.rpc();
console.log("Initialization signature:", signature);
The connected provider wallet signs automatically because authority is the transaction payer and signer. The PDA does not sign in the browser. Its signer privilege is supplied internally only where the executing program needs it for PDA creation.
Depending on the Anchor version and generated IDL configuration, PDA seed inference may fill in profile automatically when the required seed accounts are present. Explicitly deriving and passing it in tests remains valuable: it demonstrates that client and program agree on program ID, seed ordering, and encodings.
After confirmation, fetch and inspect the stored state:
const profile = await program.account.profile.fetch(profilePda);
console.log("Stored authority:", profile.authority.toBase58());
console.log("Stored bump:", profile.bump);
The fetched authority should equal the connected wallet public key, and profile.bump should equal the bump returned by findProgramAddressSync.
A reliable mental trace for PDA initialization
When debugging this pattern, trace the instruction in this order:
- The client chooses an authority and derives the expected PDA from the documented seeds and
program.programId. - The client submits the authority, PDA, and System Program in one transaction; the authority signs and funds it.
- Anchor derives the PDA again from
seedsandbumpduring account validation. - Anchor confirms the supplied
profileaddress is that expected PDA. initinvokes the System Program, using the PDA seeds as program signer seeds, creates the account, allocates its declared space, and assigns it to your program.- The handler writes the initial serialized
Profilefields. - Anchor serializes the resulting account data, including its discriminator.
If something fails, classify it before changing code:
| Symptom | Likely cause |
|---|---|
| PDA constraint violation | Client and program used different program IDs, seed order, or seed bytes. |
| Insufficient funds | The payer lacks enough SOL for transaction fees and the new account’s storage deposit. |
| “Account already in use” or initialization failure on retry | The deterministic PDA account already exists. |
| Deserialization or space error | space does not accommodate the full account discriminator and serialized fields. |
| Missing signature or writable-account error | The declared payer is not a mutable signer in the context or transaction. |
Key takeaways
init,seeds,bump,payer,space, andsystem_programtogether create a program-owned Anchor account at a deterministic PDA.seedsand canonicalbumpvalidate that the supplied account is the one PDA your protocol expects.- The payer funds allocation, but does not own the account data; the executing program owns it.
- Allocate
8 + Type::INIT_SPACEfor a standard Anchor account, accounting carefully for any variable-length fields. - Store the canonical bump with PDA state when the program will later need the exact signer seeds.
- A PDA initializer establishes deterministic state; later instructions must still enforce their own authority rules.
Next, you will use a PDA as a signer in a cross-program invocation with invoke_signed, which is what lets program-controlled accounts participate in System Program and token-program operations without any private key.
Can't find a good explanation? Sign up and we'll make it for you
Sign up