Plasma chain is now live on Chainstack! Get a reliable Plasma Mainnet and Testnet RPC endpoints.    Learn more
  • Pricing
  • Docs

Solana Anchor Accounts: Seeds, Bumps, PDAs, and How the Client really works

Created Nov 13, 2025 Updated Nov 13, 2025

What is Anchor, and how does it represent accounts and constraints in Rust

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.

This post walks through building a small Anchor program that creates, initializes, and reads a user-owned PDA derived from predictable seeds. You’ll see how Anchor enforces account constraints, allocates on-chain space with InitSpace, and stores the bump for safe future validation.

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

  • Rust DSL for accounts & constraints (declarative checks instead of manual AccountInfo plumbing),
  • toolchain (build, deploy, test, IDL generation),
  • and a TypeScript client that auto-types your program interface from the IDL.

In practice, Anchor gives you:

  • #[account] Rust structs that map 1:1 to on-chain data accounts,
  • #[derive(Accounts)] contexts with constraints like initseedsbumphas_oneclose (Will be covered soon)
  • auto-generated IDL + a TypeScript client (@coral-xyz/anchor) to call your instructions safely.

More about the Anchor directory layout can be found in the Anchor documentation. We will cover only the interesting part: How the program behaves on-chain.

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)

  • create_user(name): creates and initializes the PDA with ownernamecreated_atbump.

Program Id (why it matters)

declare_id!("<YOUR-PROGRAM-ID>");
  • Anchor bakes this value into the binary.
  • At runtime, it must equal the on-chain program account you’re invoking, or you’ll get DeclaredProgramIdMismatch.
  • Make sure declare_id!Anchor.toml, and your client all use the same pubkey before you build/deploy.

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
}
  • Anchor will compute UserAccount::INIT_SPACE.
  • When initializing, allocate 8 + UserAccount::INIT_SPACE (the extra 8 is the discriminator).
  • Enforce the length at runtime (require!(name.as_bytes().len() <= 32, …)).
  • This avoids “serialize to unexpected length” failures.

PDAs, seeds, and bump

PDA derivation:

  • PDA = find_program_address(["user", authority_pubkey], program_id)
  • Why bump exists: it’s the one-byte nonce that forces the derived address off the ed25519 curve so it can be program-owned.
  • Store bump on the account so you can reference it in constraints:
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

  • init: creates the PDA and zero-inits data.
  • payer: authority funds rent.
  • space: uses 8 + INIT_SPACE (as declared in impl UserAccount).
  • seeds: uses “user” constant string and authority address.
  • bump: anchor know how to autofill it (used to find address off curve).

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:

You should see something like this after running the client:

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:

  • Derives PDAs deterministically using seeds and bump, and why storing the bump simplifies future validations.
  • Uses #[account] and #[derive(Accounts)] to define data layouts and constraints declaratively.
  • Allocates account space safely with #[max_len] and InitSpace, avoiding serialization errors.
  • Auto-generates a TypeScript client from the IDL that pre-fills PDAs and program IDs automatically.
  • Relies on consistent configuration between declare_id!, Anchor.toml, and your deployed program ID.

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.

Reliable Solana RPC infrastructure

Getting started with Solana on Chainstack is fast and straightforward. Developers can deploy a reliable Solana node within seconds through an intuitive Console — no complex setup or hardware management required. 

Chainstack provides low-latency Solana RPC access and real-time gRPC data streaming via Yellowstone Geyser Plugin, ensuring seamless connectivity for building, testing, and scaling DeFi, analytics, and trading applications. With Solana low-latency endpoints powered by global infrastructure, you can achieve lightning-fast response times and consistent performance across regions. 

Start for free, connect your app to a reliable Solana RPC endpoint, and experience how easy it is to build and scale on Solana with Chainstack – one of the best RPC providers.

SHARE THIS ARTICLE

Andrey Obruchkov

Senior Software Engineer and blockchain specialist with hands-on experience building across EVM, Solana, Bitcoin, Cosmos, Aptos, and Sui ecosystems from smart contracts and DeFi infrastructure to cross-chain integrations and developer tooling.

Customer Stories

Defined

Defined deliver real-time blockchain data for over 2M tokens and 800M NFTs with reliable Web3 infrastructure.

TrustPad

Creating a better crowdfunding environment by reducing the number of dropped requests.

DeFiato

Securing a stable environment for platform operations with ease.