First Anchor Program
Build a small counter program in Anchor one step at a time, understand every account in the instruction context, and verify the behavior with tests.
This is the lesson where Anchor stops being abstract.
You are going to build one small program, read every part of it, and test it properly.
The program is intentionally simple.
That is a feature, not a limitation.
A first Anchor program should teach the model clearly:
- one state account
- one authority
- one initialization path
- one update path
- one test loop
What You Are Building
You will build a counter program with two instructions:
initializecreates a counter accountincrementincreases the stored value by 1
That is enough for the first real build.
You do not need decrement, reset, PDA seeds, or token logic yet.
The point is to understand the full path from account validation to state change.
Before You Touch The Code
Start from a fresh Anchor workspace that already passes:
anchor build
anchor testIf the default workspace still does not pass, stop here and fix setup first.
Do not layer program logic on top of a broken environment.
Step 1: Understand The Job Of This Program
This program stores two pieces of state:
authority: who controls the countercount: the current numeric value
That gives us one simple rule:
only the stored authority should be allowed to increment the counter.
Everything in the code will support that rule.
Step 2: Replace The Generated Program With A Minimal Counter
Open:
programs/<your-program-name>/src/lib.rs
Replace the generated example with this structure.
use anchor_lang::prelude::*;
// Keep the generated program ID from your own workspace here.
declare_id!("<your-generated-program-id>");
#[program]
pub mod counter {
use super::*;
pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
let counter = &mut ctx.accounts.counter;
counter.authority = ctx.accounts.authority.key();
counter.count = 0;
Ok(())
}
pub fn increment(ctx: Context<Increment>) -> Result<()> {
let counter = &mut ctx.accounts.counter;
counter.count = counter
.count
.checked_add(1)
.ok_or(CounterError::Overflow)?;
Ok(())
}
}
#[derive(Accounts)]
pub struct Initialize<'info> {
#[account(mut)]
pub authority: Signer<'info>,
#[account(
init,
payer = authority,
space = 8 + Counter::INIT_SPACE,
)]
pub counter: Account<'info, Counter>,
pub system_program: Program<'info, System>,
}
#[derive(Accounts)]
pub struct Increment<'info> {
pub authority: Signer<'info>,
#[account(mut, has_one = authority)]
pub counter: Account<'info, Counter>,
}
#[account]
#[derive(InitSpace)]
pub struct Counter {
pub authority: Pubkey,
pub count: u64,
}
#[error_code]
pub enum CounterError {
#[msg("Counter overflow")]
Overflow,
}Keep the declare_id! value aligned with the program ID generated in your workspace.
Do not paste a fake ID and assume Anchor will sort it out later.
Step 3: Read The Program In The Right Order
Most beginners read the file top to bottom and still miss the model.
Read it in this order instead:
Counterstate structInitializeaccount contextIncrementaccount contextinitializehandlerincrementhandler
That order makes the logic easier to parse.
First, the state
#[account]
#[derive(InitSpace)]
pub struct Counter {
pub authority: Pubkey,
pub count: u64,
}This is the account data that will live on-chain.
Why each field exists:
authoritystores who is allowed to control this counter latercountstores the actual value
Then, the initialization context
#[derive(Accounts)]
pub struct Initialize<'info> {
#[account(mut)]
pub authority: Signer<'info>,
#[account(
init,
payer = authority,
space = 8 + Counter::INIT_SPACE,
)]
pub counter: Account<'info, Counter>,
pub system_program: Program<'info, System>,
}This context is doing a lot of real work.
What each piece means:
authority: Signer<'info>means the caller must sign#[account(mut)]means Anchor may debit lamports from that signer to fund account creationinitmeans create the newcounteraccountpayer = authoritymeans the signer pays rent for that new accountspace = 8 + Counter::INIT_SPACEallocates enough bytes for the discriminator plus the stored datasystem_programis required because account creation happens through the system program
That is one of the core Anchor ideas again.
The account context expresses the setup and validation before your business logic runs.
Then, the update context
#[derive(Accounts)]
pub struct Increment<'info> {
pub authority: Signer<'info>,
#[account(mut, has_one = authority)]
pub counter: Account<'info, Counter>,
}This is the security rule for the whole program.
What it means:
- the caller must sign
- the counter account can be mutated
- the stored
counter.authoritymust match the provided signer
If that has_one rule fails, increment never runs.
That is exactly what you want.
Step 4: Understand The Handlers
initialize
pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
let counter = &mut ctx.accounts.counter;
counter.authority = ctx.accounts.authority.key();
counter.count = 0;
Ok(())
}This handler does not do much.
That is good.
The context already handled the heavy setup work.
The handler only needs to write the initial state.
increment
pub fn increment(ctx: Context<Increment>) -> Result<()> {
let counter = &mut ctx.accounts.counter;
counter.count = counter
.count
.checked_add(1)
.ok_or(CounterError::Overflow)?;
Ok(())
}Two important details here:
- the handler only runs after the account checks pass
- it uses
checked_addinstead of assuming overflow never matters
That second point is small, but it is the right habit.
Even a toy program should not silently wrap numeric state.
Step 5: Build Before You Write Tests
Run:
anchor buildDo this now, before touching the test file.
If the program does not compile, the test layer will only make the problem noisier.
What you should check after the build:
- the build succeeds
- the IDL in
target/idl/updates - no errors appear around missing derives, account space, or type mismatches
Step 6: Replace The Default Test With A Counter Test
Open:
tests/<your-program-name>.ts
Replace the generated test with this version.
import * as anchor from "@coral-xyz/anchor";
import { Program } from "@coral-xyz/anchor";
describe("counter", () => {
const provider = anchor.AnchorProvider.env();
anchor.setProvider(provider);
const program = anchor.workspace.Counter as Program;
const counter = anchor.web3.Keypair.generate();
it("initializes the counter", async () => {
await program.methods
.initialize()
.accounts({
authority: provider.wallet.publicKey,
counter: counter.publicKey,
systemProgram: anchor.web3.SystemProgram.programId,
})
.signers([counter])
.rpc();
const account = await program.account.counter.fetch(counter.publicKey);
if (!account.authority.equals(provider.wallet.publicKey)) {
throw new Error("expected authority to match provider wallet");
}
if (account.count.toNumber() !== 0) {
throw new Error("expected count to start at 0");
}
});
it("increments the counter", async () => {
await program.methods
.increment()
.accounts({
authority: provider.wallet.publicKey,
counter: counter.publicKey,
})
.rpc();
const account = await program.account.counter.fetch(counter.publicKey);
if (account.count.toNumber() !== 1) {
throw new Error("expected count to be 1 after increment");
}
});
});Step 7: Read The Test Like A Client Flow
Do not treat the test file as magic either.
It is just a client calling your program.
What happens in the initialize test
await program.methods
.initialize()
.accounts({
authority: provider.wallet.publicKey,
counter: counter.publicKey,
systemProgram: anchor.web3.SystemProgram.programId,
})
.signers([counter])
.rpc();This is the client saying:
- call the
initializeinstruction - pass the authority signer
- pass the new counter account public key
- pass the system program
- sign with the newly created counter keypair because the account is being created
That last line matters.
If you forget signers([counter]), account creation will fail because the new account keypair is not participating correctly in the transaction.
What happens after the instruction runs
const account = await program.account.counter.fetch(counter.publicKey);This fetches the on-chain counter account and lets the test inspect the stored state.
That is how you prove the handler actually changed data.
Step 8: Run The Full Loop
Now run:
anchor testIf everything is wired correctly, the test flow should:
- build the program
- deploy it to localnet
- run both tests
- show success for initialize and increment
That is your first full Anchor loop.
Common Failures In This Lesson
Program name mismatch in the test
This line depends on the generated workspace name:
const program = anchor.workspace.Counter as Program;If your actual program name is different, update it.
The test has to match your workspace, not a copied tutorial name.
Wrong declare_id!
If declare_id! is out of sync with the workspace configuration, build or deploy steps can fail in confusing ways.
Keep the generated program ID consistent.
Missing signers([counter])
The initialize path creates a new account.
If the new keypair does not sign, initialization fails.
Missing mut on the counter account
If the counter will be written to, the account context must mark it mutable.
Forgetting has_one = authority
If you remove this, anyone who can provide the account could increment it.
That is exactly the kind of access-control bug Anchor is meant to help you avoid.
Why This Lesson Matters
This one small program teaches the full Anchor pattern:
- define state
- define account contexts
- express security rules in constraints
- write thin handlers
- prove the behavior with tests
That pattern is the foundation for everything later:
- PDAs
- token flows
- CPI
- escrow
One Good Extension, Not Five
If you want one extra exercise after this lesson, add:
set_count(value: u64)
That extension is useful because it forces you to repeat the exact same pattern with one instruction argument.
Do not add five more features yet.
The point of the first program is clarity.
What Comes Next
The next lesson changes one major thing.
Instead of letting accounts live at arbitrary keypairs, you will derive them deterministically with PDAs.