Create your own
Lesson illustration

Creating Token Accounts via CPI

Hello! Welcome back to our module on working with Solana tokens.

In our last lesson, you learned how to perform a Cross-Program Invocation (CPI) to create a new SPL Token Mint. This mint acts as the central authority and definition for our new token.

However, a mint on its own is just a blueprint. For users to actually own and trade your token, they need a place to store it. This lesson addresses that exact need. Our goal is to perform a CPI to the Associated Token Account program to create a token account for a user. This is the essential next step in building any token-based application on Solana.

We'll cover:

  • The role and structure of Associated Token Accounts (ATAs).
  • Why their deterministic nature is a crucial design pattern on Solana.
  • How to use Anchor's powerful init_if_needed constraint to create ATAs idempotently and securely.

What is an Associated Token Account (ATA)?

First, let's clarify the distinction again:

  • Mint Account: A single account that defines a token (e.g., its total supply, who can mint more). There is only one mint account per token type.
  • Token Account: An account that holds a balance of a specific token for a specific owner. If you want to hold three different types of tokens (e.g., USDC, RAY, and our new token), you will need three separate token accounts.

While you can create a token account at any random address, the Solana ecosystem has standardized on a special type of token account called an Associated Token Account (ATA). An ATA is simply a token account whose address is a Program Derived Address (PDA) derived from two things: a user's main wallet address and the token's mint address.

The official Anchor documentation provides an excellent definition and explains the derivation logic.

Create a Token Account

Let's start with the official definition from the Anchor documentation, 'Create a Token Account'. This will clarify what an ATA is and how its address is derived.

Please read the section 'What is an Associated Token Account?'. Pay close attention to the get_associated_token_address_and_bump_seed_internal function signature. It shows the exact inputs used to derive the PDA: the wallet address and the token mint address.

This deterministic nature is a massive user experience improvement. Imagine a dApp needing to send you a token. Without ATAs, the dApp would have to ask you, "What is the public key of the token account you created for my token?" You'd have to find it and provide it. With ATAs, the dApp already knows your wallet address and the token's mint address, so it can calculate your ATA address on its own. It doesn't need to ask you for anything.

To visualize this relationship, the following video offers a clear diagram and explanation.

A simple introduction to Solana accounts, rent, and PDAs

This video, 'A simple introduction to Solana accounts, rent, and PDAs', provides a great visual explanation of how ATAs work as PDAs.

Watch the clip from 19:52 to 21:17. Focus on the chart showing how Alice's and Bob's wallets, combined with the Associated Token Program, create unique PDAs (their token accounts) for holding a specific token. This illustrates the deterministic link between a user's wallet and their token account.

Creating an ATA: The Modern Anchor Way

So, how do we create one of these accounts from our program? Our program needs to make a CPI to the official Associated Token Account Program. This program's sole purpose is to create these special PDA token accounts.

While you could build this CPI manually, it's cumbersome. As you can see in this Stack Exchange thread, doing it by hand can lead to tricky errors like insufficient account keys.

Fortunately, Anchor provides an incredibly elegant solution that handles the entire CPI for us with just a few lines of declarative code in our Accounts struct. The key is the init_if_needed constraint.

The official Anchor documentation provides the canonical example of how to implement this.

Create a Token Account

Let's dive into the implementation using the Anchor docs. This is the standard, idiomatic way to create ATAs in modern Solana programs.

Please read the sections 'Usage', 'associated_token constraints', and the full code example under 'Create Associated Token Account'. Focus on the CreateTokenAccount struct and the #[account(...)] macro on the token_account field.

Let's break down that token_account definition. This is the heart of today's lesson.

#[derive(Accounts)]
pub struct CreateTokenAccount<'info> {
    #[account(mut)]
    pub signer: Signer<'info>, // The user who will own the new ATA
    #[account(
        init_if_needed, // Create the account only if it doesn't exist
        payer = signer, // The user pays the rent for the new account
        associated_token::mint = mint, // The mint this ATA is for
        associated_token::authority = signer, // The user is the authority of the ATA
        associated_token::token_program = token_program, // The SPL Token program
    )]
    pub token_account: InterfaceAccount<'info, TokenAccount>,
    pub mint: InterfaceAccount<'info, Mint>,
    pub token_program: Interface<'info, TokenInterface>,
    pub associated_token_program: Program<'info, AssociatedToken>,
    pub system_program: Program<'info, System>,
}

Here’s what each constraint does:

  • init_if_needed: This is perfect for ATAs. Unlike init, it won't fail if the account already exists. It simply ensures the account is ready to be used, creating it only if necessary. This makes your instruction idempotent—you can call it multiple times without causing an error. You must enable the init-if-needed feature in your Cargo.toml for this to work, as noted in the Stack Exchange answer.
  • payer = signer: The user calling the instruction (signer) will pay the lamports required to make the new ATA rent-exempt.
  • associated_token::mint = mint: This tells Anchor which token mint the ATA is being created for. Anchor uses this, along with the authority, to derive the correct PDA address.
  • associated_token::authority = signer: This is crucial. It sets the signer (the user) as the authority of the new token account, giving them control to transfer or burn the tokens inside it.
  • associated_token::token_program: Specifies which token program to use.

Notice that the instruction's body (create_token_account) is empty! All the work—the CPI to the Associated Token Account program, which in turn calls the System Program and Token Program—is handled by Anchor before your instruction logic even runs.

Client-Side Integration

Your background in front-end development makes this next part particularly relevant. How does our client-side code (in TypeScript) know what address to pass for the token_account?

It uses a helper function from the @solana/spl-token library called getAssociatedTokenAddress. The client calculates the same deterministic address that the program expects.

The Stack Exchange answer you skimmed earlier has a perfect, practical example of this in its test script.

Creating an Associated Token Account via Solana Program

Let's look at the client-side test from the Stack Exchange post 'Creating an Associated Token Account via Solana Program'. This shows how to bridge the gap between your front end and your Anchor program.

Read the section of the answer that begins 'You can then call the function in your Anchor tests file like this'. Observe how getAssociatedTokenAddress is used to find the tokenAccountAddress, which is then passed into the program.methods.initialize().accounts({...}) call.

As you can see, the flow is seamless:

  1. Client: Uses getAssociatedTokenAddress to calculate the ATA address.
  2. Client: Calls the Anchor program, passing this address in the .accounts() block.
  3. Program: Anchor receives the call, sees the init_if_needed constraint, and verifies that the token_account address passed by the client matches the PDA derived from the mint and authority. If it matches and the account doesn't exist, Anchor creates it via CPI.
Test your understanding!

In the CreateTokenAccount struct, the associated_token::authority is set to signer. This gives the user control over their own token account.

Imagine you are building an escrow service. You need your program to hold a user's tokens temporarily in an ATA. In this scenario, who should be the authority of the ATA? How would you modify the associated_token::authority constraint to achieve this?

Show answer

For an escrow account controlled by the program, the authority of the ATA should not be the user (signer), but rather a PDA owned by your program.

You would modify the constraint like this:

  1. You would have another account in your context, let's call it escrow_pda, which is a PDA of your program.
  2. You would change the constraint to associated_token::authority = escrow_pda.

This creates an ATA where your program's escrow_pda is the only authority that can sign to move the tokens out of that ATA, effectively placing them in your program's custody.

Conclusion

In this lesson, you've mastered a fundamental pattern in Solana development: creating Associated Token Accounts. This allows users to have a standardized place to receive and hold SPL tokens.

Here are the key takeaways:

  • Associated Token Accounts (ATAs) are the standard for user token storage, providing deterministic addresses derived from a user's wallet and the token's mint.
  • The Associated Token Account Program is a system-level helper program that manages the creation of ATAs.
  • Anchor's init_if_needed constraint, combined with the associated_token::* constraints, is the modern, secure, and idempotent way to create ATAs via a CPI.
  • On the client side, the @solana/spl-token library's getAssociatedTokenAddress function is used to derive the same ATA address, ensuring the client and program are in sync.

Now that we have created a token mint and have a way to create token accounts for users, the final step is to put tokens into those accounts. In the next lesson, we will implement the logic to mint new tokens from our program-controlled mint into a user's token account.

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

Sign up