Anchor

How to Create a Token with Rust and Anchor

This guide demonstrates how to create a fungible token with metadata on Solana using Rust, the Anchor framework, and the Metaplex Token Metadata program via CPI.

What You'll Build

A single Anchor instruction that:

  • Creates a new SPL token mint
  • Creates the associated token account for the payer
  • Creates a metadata account with name, symbol, and URI
  • Mints an initial token supply to the payer

Summary

Create a fungible SPL token on Solana with Anchor (Rust), mint an initial supply, and attach Metaplex Token Metadata (name, symbol, URI) via CPI.

  • One instruction: init mint + ATA + metadata, then mint supply
  • Uses: SPL Token + Metaplex Token Metadata CPI
  • Tested: Anchor 0.32.1, Solana Agave 3.1.6
  • Fungible only; NFTs need Master Edition + decimals=0 + supply=1

Out of Scope

Token-2022 extensions, confidential transfers, authority revocation, metadata updates, full NFT flow, mainnet deployment.

Quick Start

Jump to: Program · Test Client · Common Errors

  1. anchor init anchor-spl-token
  2. Add anchor-spl with metadata feature to Cargo.toml
  3. Clone Token Metadata program in Anchor.toml for localnet
  4. Paste the program code and run anchor test

Prerequisites

  • Rust installed (rustup.rs)
  • Solana CLI installed (docs.solana.com)
  • Anchor CLI installed (cargo install --git https://github.com/coral-xyz/anchor anchor-cli)
  • Node.js and Yarn for running tests
  • A Solana wallet with SOL for transaction fees

Tested Configuration

This guide was tested with the following versions:

ToolVersion
Anchor CLI0.32.1
Solana CLI3.1.6 (Agave)
Rust1.92.0
Node.js22.15.1
Yarn1.22.x

Initial Setup

Start by initializing a new Anchor project:

anchor init anchor-spl-token
cd anchor-spl-token

Configure Cargo.toml

Update programs/anchor-spl-token/Cargo.toml:

programs/anchor-spl-token/Cargo.toml
1[package]
2name = "anchor-spl-token"
3version = "0.1.0"
4description = "Created with Anchor"
5edition = "2021"
6
7[lib]
8crate-type = ["cdylib", "lib"]
9name = "anchor_spl_token"
10
11[lints.rust]
12unexpected_cfgs = { level = "warn", check-cfg = [
13 'cfg(feature, values("custom-heap", "custom-panic", "anchor-debug"))'
14] }
15
16[features]
17default = []
18cpi = ["no-entrypoint"]
19no-entrypoint = []
20no-idl = []
21no-log-ix-name = []
22idl-build = ["anchor-lang/idl-build", "anchor-spl/idl-build"]
23
24[dependencies]
25anchor-lang = "0.32.1"
26anchor-spl = { version = "0.32.1", features = ["token", "metadata", "associated_token"] }

Important

The idl-build feature must include anchor-spl/idl-build or you'll get errors like no function or associated item named 'create_type' found for struct 'anchor_spl::token::Mint'.

Configure Anchor.toml

Update Anchor.toml to clone the Token Metadata program for local testing:

Anchor.toml
1[toolchain]
2package_manager = "yarn"
3
4[features]
5resolution = true
6skip-lint = false
7
8[programs.localnet]
9anchor_spl_token = "YOUR_PROGRAM_ID_HERE"
10
11[registry]
12url = "https://api.apr.dev"
13
14[provider]
15cluster = "localnet"
16wallet = "~/.config/solana/id.json"
17
18[scripts]
19test = "yarn run ts-mocha -p ./tsconfig.json -t 1000000 tests/**/*.ts"
20
21[test.validator]
22url = "https://api.mainnet-beta.solana.com"
23bind_address = "127.0.0.1"
24
25[[test.validator.clone]]
26address = "metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s"

  • bind_address = "127.0.0.1" is required for Agave 3.x validators (0.0.0.0 causes a panic)
  • The [[test.validator.clone]] section clones the Metaplex Token Metadata program from mainnet

Configure package.json

package.json
1{
2 "license": "ISC",
3 "scripts": {
4 "lint:fix": "prettier */*.js \"*/**/*{.js,.ts}\" -w",
5 "lint": "prettier */*.js \"*/**/*{.js,.ts}\" --check"
6 },
7 "dependencies": {
8 "@coral-xyz/anchor": "^0.32.1",
9 "@metaplex-foundation/mpl-token-metadata": "^3.4.0",
10 "@solana/spl-token": "^0.4.9"
11 },
12 "devDependencies": {
13 "chai": "^4.3.4",
14 "mocha": "^9.0.3",
15 "ts-mocha": "^10.0.0",
16 "@types/bn.js": "^5.1.0",
17 "@types/chai": "^4.3.0",
18 "@types/mocha": "^9.0.0",
19 "typescript": "^5.7.3",
20 "prettier": "^2.6.2"
21 }
22}

The Program

Imports and Template

Here we define all the imports and create the template for the Account struct and instruction in programs/anchor-spl-token/src/lib.rs:

programs/anchor-spl-token/src/lib.rs
1use anchor_lang::prelude::*;
2use anchor_spl::{
3 associated_token::AssociatedToken,
4 metadata::{
5 create_metadata_accounts_v3, mpl_token_metadata::types::DataV2, CreateMetadataAccountsV3,
6 Metadata,
7 },
8 token::{mint_to, Mint, MintTo, Token, TokenAccount},
9};
10
11declare_id!("YOUR_PROGRAM_ID_HERE");
12
13#[program]
14pub mod anchor_spl_token {
15 use super::*;
16
17 pub fn create_token(
18 ctx: Context<CreateToken>,
19 name: String,
20 symbol: String,
21 uri: String,
22 decimals: u8,
23 amount: u64,
24 ) -> Result<()> {
25 Ok(())
26 }
27}
28
29#[derive(Accounts)]
30#[instruction(name: String, symbol: String, uri: String, decimals: u8)]
31pub struct CreateToken<'info> {
32
33}

Creating the Account Struct

The CreateToken struct defines all accounts required by the instruction and applies the necessary constraints:

programs/anchor-spl-token/src/lib.rs
1#[derive(Accounts)]
2#[instruction(name: String, symbol: String, uri: String, decimals: u8)]
3pub struct CreateToken<'info> {
4 #[account(mut)]
5 pub payer: Signer<'info>,
6
7 /// The mint account to be created
8 #[account(
9 init,
10 payer = payer,
11 mint::decimals = decimals,
12 mint::authority = payer.key(),
13 mint::freeze_authority = payer.key(),
14 )]
15 pub mint: Account<'info, Mint>,
16
17 /// The associated token account to receive minted tokens
18 #[account(
19 init,
20 payer = payer,
21 associated_token::mint = mint,
22 associated_token::authority = payer,
23 )]
24 pub token_account: Account<'info, TokenAccount>,
25
26 /// The metadata account to be created
27 /// CHECK: Validated by seeds constraint to be the correct PDA
28 #[account(
29 mut,
30 seeds = [
31 b"metadata",
32 token_metadata_program.key().as_ref(),
33 mint.key().as_ref(),
34 ],
35 bump,
36 seeds::program = token_metadata_program.key(),
37 )]
38 pub metadata_account: UncheckedAccount<'info>,
39
40 pub token_program: Program<'info, Token>,
41 pub token_metadata_program: Program<'info, Metadata>,
42 pub associated_token_program: Program<'info, AssociatedToken>,
43 pub system_program: Program<'info, System>,
44 pub rent: Sysvar<'info, Rent>,
45}

Account Types:

  • The #[instruction(...)] attribute allows using instruction arguments (like decimals) in account constraints
  • mint uses Anchor's init constraint with mint::decimals = decimals to create the token mint with the specified decimal places
  • token_account is initialized as an associated token account using associated_token:: helpers
  • metadata_account uses seeds::program to validate the PDA belongs to the Token Metadata program

Creating the Instruction

The create_token function creates the metadata account via CPI and mints the initial token supply:

programs/anchor-spl-token/src/lib.rs
1pub fn create_token(
2 ctx: Context<CreateToken>,
3 name: String,
4 symbol: String,
5 uri: String,
6 decimals: u8,
7 amount: u64,
8) -> Result<()> {
9 msg!("Creating token mint...");
10 msg!("Mint: {}", ctx.accounts.mint.key());
11 msg!("Creating metadata account...");
12 msg!("Metadata account address: {}", ctx.accounts.metadata_account.key());
13
14 // Cross Program Invocation (CPI) to token metadata program
15 create_metadata_accounts_v3(
16 CpiContext::new(
17 ctx.accounts.token_metadata_program.to_account_info(),
18 CreateMetadataAccountsV3 {
19 metadata: ctx.accounts.metadata_account.to_account_info(),
20 mint: ctx.accounts.mint.to_account_info(),
21 mint_authority: ctx.accounts.payer.to_account_info(),
22 update_authority: ctx.accounts.payer.to_account_info(),
23 payer: ctx.accounts.payer.to_account_info(),
24 system_program: ctx.accounts.system_program.to_account_info(),
25 rent: ctx.accounts.rent.to_account_info(),
26 },
27 ),
28 DataV2 {
29 name,
30 symbol,
31 uri,
32 seller_fee_basis_points: 0,
33 creators: None,
34 collection: None,
35 uses: None,
36 },
37 true, // is_mutable
38 true, // update_authority_is_signer
39 None, // collection_details
40 )?;
41
42 // Mint tokens to the payer's associated token account
43 msg!("Minting {} tokens to {}", amount, ctx.accounts.token_account.key());
44
45 mint_to(
46 CpiContext::new(
47 ctx.accounts.token_program.to_account_info(),
48 MintTo {
49 mint: ctx.accounts.mint.to_account_info(),
50 to: ctx.accounts.token_account.to_account_info(),
51 authority: ctx.accounts.payer.to_account_info(),
52 },
53 ),
54 amount,
55 )?;
56
57 msg!("Token created and {} tokens minted successfully.", amount);
58 Ok(())
59}

The function performs two Cross-Program Invocations:

  1. create_metadata_accounts_v3 (lines 14-40) - Creates and initializes the metadata account with name, symbol, and URI
  2. mint_to (lines 43-54) - Mints the specified amount to the payer's token account

The Client

Before testing, build the program:

anchor build

Get your program ID and update it in both lib.rs and Anchor.toml:

solana address -k target/deploy/anchor_spl_token-keypair.json

Then rebuild and deploy:

anchor build
anchor deploy

Creating the Test

Create the test file at tests/anchor-spl-token.ts:

tests/anchor-spl-token.ts
1import * as anchor from "@coral-xyz/anchor";
2import { Program } from "@coral-xyz/anchor";
3import { AnchorSplToken } from "../target/types/anchor_spl_token";
4import { Keypair, PublicKey, SystemProgram, SYSVAR_RENT_PUBKEY } from "@solana/web3.js";
5import { TOKEN_PROGRAM_ID, ASSOCIATED_PROGRAM_ID } from "@coral-xyz/anchor/dist/cjs/utils/token";
6import { getAssociatedTokenAddressSync } from "@solana/spl-token";
7import { BN } from "bn.js";
8
9const TOKEN_METADATA_PROGRAM_ID = new PublicKey(
10 "metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s"
11);
12
13describe("anchor-spl-token", () => {
14 const provider = anchor.AnchorProvider.env();
15 anchor.setProvider(provider);
16
17 const program = anchor.workspace.AnchorSplToken as Program<AnchorSplToken>;
18 const payer = provider.wallet;
19
20 it("Creates a token with metadata and mints initial supply", async () => {
21 const mintKeypair = Keypair.generate();
22
23 const tokenName = "My Token";
24 const tokenSymbol = "MYTKN";
25 const tokenUri = "https://example.com/token-metadata.json";
26 const tokenDecimals = 9;
27 const mintAmount = new BN(1_000_000).mul(new BN(10).pow(new BN(tokenDecimals)));
28
29 // Derive the metadata account PDA
30 const [metadataAccount] = PublicKey.findProgramAddressSync(
31 [
32 Buffer.from("metadata"),
33 TOKEN_METADATA_PROGRAM_ID.toBuffer(),
34 mintKeypair.publicKey.toBuffer(),
35 ],
36 TOKEN_METADATA_PROGRAM_ID
37 );
38
39 // Derive the associated token account
40 const tokenAccount = getAssociatedTokenAddressSync(
41 mintKeypair.publicKey,
42 payer.publicKey
43 );
44
45 console.log("Mint address:", mintKeypair.publicKey.toBase58());
46 console.log("Metadata address:", metadataAccount.toBase58());
47 console.log("Token account:", tokenAccount.toBase58());
48
49 const tx = await program.methods
50 .createToken(tokenName, tokenSymbol, tokenUri, tokenDecimals, mintAmount)
51 .accountsPartial({
52 payer: payer.publicKey,
53 mint: mintKeypair.publicKey,
54 tokenAccount: tokenAccount,
55 metadataAccount: metadataAccount,
56 tokenProgram: TOKEN_PROGRAM_ID,
57 tokenMetadataProgram: TOKEN_METADATA_PROGRAM_ID,
58 associatedTokenProgram: ASSOCIATED_PROGRAM_ID,
59 systemProgram: SystemProgram.programId,
60 rent: SYSVAR_RENT_PUBKEY,
61 })
62 .signers([mintKeypair])
63 .rpc();
64
65 console.log("Transaction signature:", tx);
66 console.log("Token created and minted successfully!");
67 });
68});

Key Points:

  • The metadata account PDA is derived using seeds: ["metadata", TOKEN_METADATA_PROGRAM_ID, mint_pubkey] (lines 29-36)
  • The associated token account is derived using getAssociatedTokenAddressSync (lines 39-42)
  • The mint keypair must be passed as a signer since it's being initialized
  • Use accountsPartial to specify accounts (Anchor 0.32+ syntax)
  • Use BN for large numbers (token amounts with decimals)
  • tokenDecimals is passed to the instruction and used to calculate the mint amount

Running the Test

yarn install
anchor test

Expected output:

anchor-spl-token
Mint address: GpPyH2FuMcS5PcrKWtrmEkBmW8h8gSwUaxNCQkFXwifV
Metadata address: 6jskfrDAmH9d67iL37CLNBK7Hf6FRwNZbq34q4vGucDq
Token account: J3KCxCfmnK9RJ3onmiUsfBDjvKyuVsAXgWvuypsaFQ2i
Transaction signature: 36v63t5cCsXYM8ny4pgahh...
Token created and minted successfully!
✔ Creates a token with metadata and mints initial supply (243ms)
1 passing (245ms)

Metadata JSON Format

The uri field should point to a JSON file containing your token's off-chain metadata:

token-metadata.json
{
"name": "My Token",
"symbol": "MYTKN",
"description": "A description of my token",
"image": "https://example.com/token-image.png"
}

Host this JSON file on a permanent storage solution like Arweave or IPFS.

Common Errors

no function or associated item named 'create_type' found

Add "anchor-spl/idl-build" to the idl-build feature in Cargo.toml:

idl-build = ["anchor-lang/idl-build", "anchor-spl/idl-build"]

Program account is not executable

Clone the Token Metadata program in Anchor.toml:

[[test.validator.clone]]
address = "metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s"

UnspecifiedIpAddr(0.0.0.0) / Validator panic

Add bind_address = "127.0.0.1" to [test.validator] in Anchor.toml.

Notes

  • The amount parameter is in base units (includes decimals). For 1 million tokens with 9 decimals, pass 1_000_000 * 10^9.
  • This example keeps mint authority and freeze authority on the payer. Production tokens often revoke or transfer these authorities after initial minting.
  • The metadata account is mutable (is_mutable = true). Set to false if you want immutable metadata.

Next Steps

  • Deploy to Devnet: Change cluster = "devnet" in Anchor.toml and run anchor deploy
  • Create an NFT: Set decimals = 0 and supply = 1 for non-fungible tokens
  • Add Token Extensions: Explore SPL Token 2022 for transfer fees, interest-bearing tokens, and more
  • Learn more about Token Metadata: See the Token Metadata documentation

Quick Reference

Key Program IDs

ProgramAddress
Token ProgramTokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA
Associated Token ProgramATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL
Token Metadata ProgrammetaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s
System Program11111111111111111111111111111111

Metadata PDA Seeds

Deriving the Metadata PDA

1const [metadataAccount] = PublicKey.findProgramAddressSync(
2 [Buffer.from("metadata"), TOKEN_METADATA_PROGRAM_ID.toBuffer(), mint.toBuffer()],
3 TOKEN_METADATA_PROGRAM_ID
4);

Minimum Dependencies

Cargo.toml
anchor-lang = "0.32.1"
anchor-spl = { version = "0.32.1", features = ["token", "metadata", "associated_token"] }

FAQ

Terminology

  • Fungible token: decimals >= 0, unlimited supply potential
  • NFT: decimals = 0, supply = 1, plus Master Edition account
  • Token Metadata: Metaplex program used for both fungible tokens and NFTs
  • SPL: Solana Program Library, the standard token interface

What is an SPL token?

An SPL token is Solana's equivalent of an ERC-20 token on Ethereum. SPL stands for Solana Program Library. SPL tokens are fungible tokens that can represent currencies, governance tokens, stablecoins, or any other fungible asset on Solana.

What is the difference between a token mint and a token account?

  • Token Mint: The factory that creates tokens. It defines the token's properties (decimals, supply, authorities). There is one mint per token type.
  • Token Account: A wallet that holds tokens. Each user needs their own token account for each token type they want to hold.

What is an Associated Token Account (ATA)?

An Associated Token Account is a deterministically-derived token account for a given wallet and mint. Instead of creating random token accounts, ATAs use a standard derivation so anyone can calculate the token account address for any wallet. This is the recommended way to handle token accounts.

What is Metaplex Token Metadata?

Metaplex Token Metadata is a program that attaches metadata (name, symbol, image URI) to SPL tokens. Without it, tokens are just anonymous mints. The metadata is stored in a Program Derived Address (PDA) associated with the mint.

Why clone the Token Metadata program for local testing?

The local Solana test validator starts with a clean state and doesn't include any programs except the core Solana programs. Metaplex Token Metadata is a separate program deployed on mainnet, so you need to clone it to use it locally.

Can I use this code to create an NFT?

Yes, with modifications:

  • Set mint::decimals = 0 (NFTs are indivisible)
  • Mint exactly 1 token
  • Remove mint authority after minting (so no more can be created)
  • Add a Master Edition account (for Metaplex NFT standard)

How much does it cost to create a token on Solana?

Creating a token requires rent for three accounts:

  • Mint account: ~0.00145 SOL
  • Token account: ~0.00203 SOL
  • Metadata account: ~0.01 SOL

Total: approximately 0.015-0.02 SOL (varies with rent prices).

What's the difference between Anchor and native Solana Rust?

Anchor is a framework that simplifies Solana development by:

  • Auto-generating account serialization/deserialization
  • Providing declarative account validation with macros
  • Generating TypeScript clients automatically
  • Handling common patterns like PDAs and CPIs

Native Solana Rust requires manually handling all of these concerns.

Glossary

TermDefinition
SPL TokenSolana Program Library token standard, equivalent to ERC-20
MintThe account that defines a token and can create new supply
Token AccountAn account that holds a balance of a specific token
ATAAssociated Token Account - deterministic token account for a wallet
PDAProgram Derived Address - an address derived from seeds, owned by a program
CPICross-Program Invocation - calling one Solana program from another
AnchorA Rust framework for building Solana programs
MetaplexProtocol for NFTs and token metadata on Solana
IDLInterface Definition Language - describes a program's interface
RentSOL required to keep an account alive on Solana