Create your own
Lesson illustration

Initializing Accounts with `init` and `payer`

Hello! Let's continue our exploration of the Anchor framework.

In our last lesson, we focused on securing access to existing accounts. You learned to use powerful constraints like has_one and constraint to declare and enforce security rules, ensuring that only authorized signers can modify specific data.

But this raises a fundamental question: how do those data accounts get created in the first place? On Solana, unlike EVM-based chains where storage is part of the contract itself, data lives in separate accounts that must be explicitly created and funded with SOL to cover rent.

This lesson addresses exactly that. Our goal is to learn how to initialize new accounts using Anchor's init constraint and payer. By the end, you will understand the complete lifecycle of a data account, from creation to its first use.

The Need for Account Initialization

In Solana, everything is an account. Your program code lives in an account marked as "executable." The data your program operates on, like a user's profile or a game's state, lives in separate "non-executable" data accounts.

Before you can write any data (e.g., setting an initial counter value to 0), you must first perform a separate step: creating the account itself. This involves asking the Solana runtime to allocate a certain amount of memory (space) and transferring enough SOL to that account to make it rent-exempt.

This process is handled by a core, on-chain utility program called the System Program. While you could construct a raw create_account instruction to call the System Program manually, Anchor provides a much safer and more convenient abstraction: the init constraint.

The init Constraint: Your Account Creation Tool

The init constraint is the cornerstone of creating new accounts in Anchor. When you apply it to an account in your Accounts struct, you are telling Anchor to automatically insert a Cross-Program Invocation (CPI) to the System Program to create that account before your instruction logic runs.

For init to work, you must provide three critical pieces of information:

  1. space: How many bytes of storage to allocate for the new account.
  2. payer: Which account will pay the lamports (SOL) required for rent.
  3. system_program: The System Program itself, which is needed to execute the creation.

Let's look at a canonical example of an Initialize struct for creating a simple counter account.

use anchor_lang::prelude::*;

// The main program module
#[program]
pub mod basic_counter {
    use super::*;
    pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
        // We can set initial values here
        let counter_account = &mut ctx.accounts.counter;
        counter_account.count = 0;
        Ok(())
    }
}

// 1. The data structure for our new account
#[account]
pub struct Counter {
    pub count: u64, // 8 bytes
}

// 2. The Accounts context for the `initialize` instruction
#[derive(Accounts)]
pub struct Initialize<'info> {
    #[account(
        init,
        payer = user,
        space = 8 + 8
    )]
    pub counter: Account<'info, Counter>,

    #[account(mut)]
    pub user: Signer<'info>,

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

This pattern is fundamental, so let's break down the Initialize struct.

Initializing Accounts in Solana and Anchor

The article "Initializing Accounts in Solana and Anchor" from RareSkills provides an excellent, detailed walkthrough of this exact pattern. It explains each component of the initialization process.

Read the sections from "Account initialization boilerplate code" through to "What is the system program?". Pay close attention to the breakdown of the Initialize struct, the role of each field (my_storage, signer, system_program), and the explanation of the init, payer, and space parameters.

As the article explains, the magic happens in the attributes above the counter account:

  • #[account(init, ...)]: This marks counter as the account to be created. Anchor ensures this account does not already exist, preventing accidental re-initialization.
  • payer = user: This specifies that the user account (defined below it) will pay the lamports for the new counter account's rent.
  • space = 8 + 8: This defines the account's size in bytes. We'll dissect this next.
  • user: Signer<'info>: This is the fee payer. It must be a Signer to authorize the lamport transfer, and it must be marked #[account(mut)] because its SOL balance will decrease.
  • system_program: Program<'info, System>: This passes in the System Program, which init calls under the hood. Anchor requires this exact field name and type.

Calculating Account space

The space parameter is crucial. If you allocate too little, your program will fail when it tries to write data that doesn't fit. If you allocate too much, you are wasting the user's SOL on rent.

The formula is: Total Space = 8 bytes (Discriminator) + Size of Data

  • Size of Data: This is the space your custom struct requires. For our Counter struct, it's a single u64 (unsigned 64-bit integer), which is 8 bytes.
  • 8-byte Discriminator: This is an internal Anchor feature. Anchor takes the first 8 bytes of a SHA-256 hash of your struct's name (e.g., Counter) and prepends it to the account's data. When you later try to read this account, Anchor checks this discriminator to ensure the data belongs to the Counter struct, preventing a whole class of account-confusion attacks.

So, for our Counter struct, the space is 8 (discriminator) + 8 (for the u64) = 16 bytes.

A Beginner's Guide to Building Solana Programs with Anchor

For a deeper dive on account space, the Helius guide provides an excellent breakdown of data type sizes and the role of the discriminator.

Read the section "Account Space". Focus on the subsections "Sizing Variables" and "Anchor's Internal Discriminator". This will solidify your understanding of where the + 8 comes from and how to size other data types you might use in the future.

Test your understanding!

You are creating a PlayerProfile account to store a player's level and their experience points. The struct is defined as:

#[account]
pub struct PlayerProfile {
    pub level: u16,       // 2 bytes
    pub xp: u64,          // 8 bytes
    pub authority: Pubkey,  // 32 bytes
}

What value should you use for the space constraint when initializing this account?

Show answer

The total space for the data is 2 (level) + 8 (xp) + 32 (authority) = 42 bytes.

You must also add the 8-byte discriminator.

So, the correct space is 8 + 42 = 50 bytes. The constraint would be space = 8 + 2 + 8 + 32 or simply space = 50.

Seeing it in Action

Reading about it is one thing, but seeing it work provides clarity. The following video demonstrates these concepts in a live-coding format, which is a great way to see how the pieces fit together.

Anchor Basics [Solana Development Course M5P1] - Feb 22nd '23

This segment from a Solandy tutorial provides a clear, practical demonstration of initializing an account with init, payer, and space.

Watch the section "Initializing New Accounts with init, payer, and space" from 00:21:37 to 00:26:08. The presenter walks through the code, explaining the purpose of each constraint, including the 8-byte discriminator, and how they relate to the underlying System Program call.

Testing Account Initialization

Given your extensive frontend background, the testing part of Anchor, which is done in TypeScript, will feel very familiar. When testing an initialize instruction, Anchor's client-side library simplifies things greatly.

You typically need to:

  1. Generate a new Keypair for the account you are about to create.
  2. Call the program's initialize method.
  3. Pass the public key of the new keypair into the .accounts({}) object.
  4. Crucially, pass the new keypair itself into the .signers([]) array. The init constraint requires the new account's keypair to sign the transaction to prove ownership of the address being created.
  5. Ensure the payer account has enough SOL to pay for the transaction and the rent.

The next video segment continues from where the last one left off, showing exactly how to write and run a test for an instruction that initializes an account.

Anchor Basics [Solana Development Course M5P1] - Feb 22nd '23

Let's continue with the Solandy video to see how to test our initialization logic. This part is especially relevant to your experience with TypeScript.

Watch the section "Testing Account Initialization and Data Storage" from 00:34:44 to 00:41:58. Pay attention to how a new Keypair is created for the data account on the client side, how the payer is funded with an airdrop, and how the test is executed with anchor test. The presenter also uses the Solana CLI to inspect the newly created account's data on-chain, confirming the discriminator and the stored value.

As you saw in the video, a common error when testing is Transaction simulation failed: Error processing Instruction 0: custom program error: 0x1. This usually means the payer account doesn't have enough SOL. The video shows the solution: airdropping SOL to the payer's keypair before running the test.

Conclusion

You have now mastered one of the most fundamental patterns in Solana development: creating new accounts. By using Anchor's init constraint, you can abstract away the low-level details of CPIs and rent, allowing you to focus on your program's logic.

Here are the key takeaways from this lesson:

  • Solana requires explicit account initialization before data can be stored, a key difference from the EVM model.
  • The #[account(init, ...)] constraint is Anchor's declarative tool for creating and initializing new accounts.
  • init requires three companions:
    • payer: The Signer account that will pay the rent for the new account.
    • space: The size in bytes to allocate, calculated as 8 (discriminator) + size_of_your_data_struct.
    • system_program: The Solana System Program, which init calls to perform the creation.
  • The payer account must be marked as mut because its SOL balance will change.
  • Anchor protects you from accidentally re-initializing an existing account when using init.

In the next lesson, we will formalize our knowledge of writing and running integration tests. While we've already seen how to test an initialize instruction, we will now cover the testing framework more broadly, learning how to test various instructions (increment, update, etc.) and make assertions about the state of our on-chain accounts.

Can't find a good explanation? Sign up and we'll make it for you

Sign up