Start now →

Understanding Anchor Accounts: Seeds, Bumps, PDAs, and How the Client Really Works (chainstack…

By Andrey Obruchkov · Published April 20, 2026 · 7 min read · Source: Blockchain Tag
Blockchain
Understanding Anchor Accounts: Seeds, Bumps, PDAs, and How the Client Really Works (chainstack…

Understanding Anchor Accounts: Seeds, Bumps, PDAs, and How the Client Really Works (chainstack version)

Andrey ObruchkovAndrey Obruchkov6 min read·Just now

--

In this post we’ll walk through a minimal Anchor program that creates, updates, and closes a user-owned PDA, and then call it end-to-end from a TypeScript script. Along the way you’ll learn how Anchor derives accounts, when you need to pass them manually, why storing the bump matters, and how the client auto-fills everything based on your constraints.

What is Anchor and How it represents accounts and constraints in Rust

Anchor is the most popular framework for Solana programs. Think of it as:

In practice, Anchor gives you:

more about anchor directory layout can be found here. We will cover only the interesting part: How the program behaves on-chain.

Scaffold a New Program

# We assume that rustc, anchor, cargo-build-sbf and solana installed properly
anchor init solana_accounts
cd solana_accounts

Rust code

use anchor_lang::prelude::*;

declare_id!("<YOUR-PROGRAM-ID>");

#[program]
pub mod solana_accounts {
use super::*;

/// Create a PDA for the user and store their name + creation time.
pub fn create_user(ctx: Context<CreateUser>, name: String) -> Result<()> {
// Enforce max length in BYTES (UTF-8). Emojis count as multiple bytes.
require!(
name.as_bytes().len() <= UserAccount::MAX_NAME,
ErrorCode::NameTooLong
);

let user = &mut ctx.accounts.user_account;
user.owner = ctx.accounts.authority.key();
user.name = name.clone(); // fits because we sized with MAX_NAME
user.created_at = Clock::get()?.unix_timestamp;
user.bump = ctx.bumps.user_account;

Ok(())
}
}

#[account]
#[derive(InitSpace)]
pub struct UserAccount {
pub owner: Pubkey, // 32
#[max_len(32)]
pub name: String, // 4 + up to 32 bytes
pub created_at: i64, // 8
pub bump: u8, // 1
}

impl UserAccount {
pub const MAX_NAME: usize = 32;
// Total space to allocate at init time:
// 8 (discriminator) + INIT_SPACE computed by Anchor from the struct
pub const SPACE: usize = 8 + Self::INIT_SPACE;
}

#[derive(Accounts)]
pub struct CreateUser<'info> {
#[account(mut)]
pub authority: Signer<'info>,

/// PDA: seeds = ["user", authority]
#[account(
init,
payer = authority,
space = UserAccount::SPACE,
seeds = [b"user", authority.key().as_ref()],
bump
)]
pub user_account: Account<'info, UserAccount>,

pub system_program: Program<'info, System>,
}

#[error_code]
pub enum ErrorCode {
#[msg("Name too long (max 32 bytes).")]
NameTooLong,
}

Overview

What it stores: A per-wallet UserAccount at a PDA derived from [“user”, authority].

Instructions (will be covered in next blog post)

Program Id (why it matters)

declare_id!("<YOUR-PROGRAM-ID>");

The account: layout & size

#[account]
#[derive(InitSpace)]
pub struct UserAccount {
pub owner: Pubkey, // 32
#[max_len(32)]
pub name: String, // 4 + up to 32 bytes
pub created_at: i64, // 8
pub bump: u8, // 1
}

PDAs, seeds, and bump

PDA derivation:

seeds = [b"user", authority.key().as_ref()], bump = user_account.bump

Instruction: create_user

object overview

#[derive(Accounts)]
pub struct CreateUser<'info> {
#[account(mut)]
pub authority: Signer<'info>,

/// PDA: seeds = ["user", authority]
#[account(
init,
payer = authority,
space = UserAccount::SPACE,
seeds = [b"user", authority.key().as_ref()],
bump
)]
pub user_account: Account<'info, UserAccount>,
pub system_program: Program<'info, System>,
}

pub authority: Signer<'info> : This field represents the caller of the instruction, and the Signer represents that this account must sign the transaction, it have #[account(mut)] because this signer will pay for the account creation fee, so their balance will change.

pub user_account: Account<'info, UserAccount> : This is the PDA account that we are creating. It is the on-chain data structure it will store what was declared in UserAccount object.

Notes

pub system_program: Program<'info, System> : Any init must call the system program under the hood which allocate new account, assign ownership, transfer lamports from payer. This field is required for account creation, lamport transfers, PDA initialization

handler overview

pub fn create_user(ctx: Context<CreateUser>, name: String) -> Result<()> {
// Enforce max length in BYTES (UTF-8). Emojis count as multiple bytes.
require!(
name.as_bytes().len() <= UserAccount::MAX_NAME,
ErrorCode::NameTooLong
);

let user = &mut ctx.accounts.user_account;
// Stores the wallet that created this profile
user.owner = ctx.accounts.authority.key();
// This assigns into the fixed allocated space Anchor reserved via #[max_len]
user.name = name.clone(); // fits because we sized with MAX_NAME
// Clock sysvar contains the current cluster time
user.created_at = Clock::get()?.unix_timestamp;
// Why store the bump:
// we used: PDA = find_program_address(["user", authority], bump)
// We don't want to recompute bump manually later
// update_name and close_user enforce (We will see it later)
user.bump = ctx.bumps.user_account;

Ok(())
}

Context<CreateUser>: Gives you validated access to all accounts declared in the CreateUser struct. Anchor has already validated all constraints and created/allocated required accounts

Client side (Ts)

Save this file in scripts/solana_accounts.ts

Note: For this code to run we need to export 2 env variables:

export ANCHOR_PROVIDER_URL=”http://127.0.0.1:8899”

export ANCHOR_WALLET=”$HOME/.config/solana/id.json”

import * as anchor from "@coral-xyz/anchor";
import type { Program } from "@coral-xyz/anchor";
import { LAMPORTS_PER_SOL, PublicKey } from "@solana/web3.js";
import { SolanaAccounts } from "../target/types/solana_accounts";

(async () => {
const provider = anchor.AnchorProvider.env();
anchor.setProvider(provider);

// Use Anchor workspace (uses generated IDL/types under target/)
const program = anchor.workspace.solanaAccounts as Program<SolanaAccounts>;
const wallet = provider.wallet as anchor.Wallet;

// PDA: seeds = ["user", authority]
// Seeds must match the program's #[account(seeds = [b"user", authority])]. From Rust!
const [userPda] = PublicKey.findProgramAddressSync([Buffer.from("user"), wallet.publicKey.toBuffer()], program.programId);
console.log("Wallet:", wallet.publicKey.toBase58());
console.log("Program:", program.programId.toBase58());
console.log("User PDA:", userPda.toBase58());

// 1) createUser
// Derivable accounts (PDAs) are autofilled by Anchor
// No need to pass programId or PDA here! it knows from the context.
const sig1 = await program.methods
.createUser("ByteBeetle")
.accounts({ authority: wallet.publicKey }) // derivable accounts are autofilled
.rpc();
console.log("createUser tx:", sig1);

// Fethch and log the created account
const acct1 = await program.account.userAccount.fetch(userPda);
const createdAtBn = (acct1 as any).createdAt ?? (acct1 as any).created_at;
const createdAt = new Date(createdAtBn.toNumber() * 1000).toISOString();
console.log("After create:", {
owner: acct1.owner.toBase58(),
name: acct1.name,
created_at: createdAt,
bump: acct1.bump,
});

console.log("Done ✅");
})().catch((e) => {
console.error(e);
process.exit(1);
});

Run it all together

# In separate terminal
solana-test-validator -r

# New tab or new terminal
solana config set --url http://127.0.0.1:8899
solana config set --keypair ~/.config/solana/id.json
# Run this in project root
solana-keygen new -o target/deploy/solana_accounts-keypair.json --no-bip39-passphrase
solana-keygen pubkey target/deploy/solana_accounts-keypair.json
# Paste this pubkey into:
# - programs/solana_accounts/src/lib.rs: declare_id!("...")
# - Anchor.toml: [programs.localnet].solana_accounts = "..."

anchor clean
anchor build
anchor deploy

# Reminder: you need to run this before:
# export ANCHOR_PROVIDER_URL="http://127.0.0.1:8899"
# export ANCHOR_WALLET="$HOME/.config/solana/id.json"

pnpm ts-node scripts/solana_accounts.ts

You should see something like this after deployment:

Press enter or click to view image in full size

You should see something like this after running the client:

Press enter or click to view image in full size

Summary

In this post, we built a minimal Anchor program that creates a per-user PDA and interacted with it end-to-end using the TypeScript client.
You learned how Anchor:

By understanding how Anchor resolves accounts and constraints behind the scenes, you can reason better about what actually happens on-chain and debug or extend your Solana programs with confidence.

This article was originally published on Blockchain Tag and is republished here under RSS syndication for informational purposes. All rights and intellectual property remain with the original author. If you are the author and wish to have this article removed, please contact us at [email protected].

NexaPay — Accept Card Payments, Receive Crypto

No KYC · Instant Settlement · Visa, Mastercard, Apple Pay, Google Pay

Get Started →