Builder· Lesson 4 of 8· 4 min
Tokens from code: mint, ATA, transfer, metadata
The five instructions behind “create a token”, written out with @solana/spl-token: a mint, its metadata, an associated token account, the supply, and the revokes. Then the same mint on Token-2022 with a transfer fee.
The mint
A mint is an 82-byte account owned by the Token program. Creating it is two instructions: the System Program allocates the account (you pay its rent and the new keypair signs), and the Token program initialises it with decimals and authorities.
import { Keypair, SystemProgram } from "@solana/web3.js";
import {
MINT_SIZE,
TOKEN_PROGRAM_ID,
createInitializeMint2Instruction,
getMinimumBalanceForRentExemptMint,
} from "@solana/spl-token";
const mint = Keypair.generate(); // the mint's address; it signs its own creation
const decimals = 6;
const createMintAccount = SystemProgram.createAccount({
fromPubkey: payer,
newAccountPubkey: mint.publicKey,
space: MINT_SIZE, // 82 bytes
lamports: await getMinimumBalanceForRentExemptMint(connection),
programId: TOKEN_PROGRAM_ID, // the owner from birth
});
const initMint = createInitializeMint2Instruction(
mint.publicKey,
decimals,
payer, // mint authority
payer, // freeze authority (null for none)
TOKEN_PROGRAM_ID
);Metadata
import {
PROGRAM_ID as METADATA_PROGRAM_ID,
createCreateMetadataAccountV3Instruction,
} from "@metaplex-foundation/mpl-token-metadata";
const [metadata] = PublicKey.findProgramAddressSync(
[Buffer.from("metadata"), METADATA_PROGRAM_ID.toBuffer(), mint.publicKey.toBuffer()],
METADATA_PROGRAM_ID
);
const createMetadata = createCreateMetadataAccountV3Instruction(
{ metadata, mint: mint.publicKey, mintAuthority: payer, payer, updateAuthority: payer },
{
createMetadataAccountArgsV3: {
data: {
name: "Example",
symbol: "EXM",
uri: "https://gateway.irys.xyz/<id>", // JSON with description + image, uploaded first
sellerFeeBasisPoints: 0,
creators: null,
collection: null,
uses: null,
},
isMutable: true,
collectionDetails: null,
},
}
);The metadata PDA is derived exactly as Token metadata describes; the instruction must be signed by the mint authority, which is why it goes in the same transaction as the mint while you still hold it. Upload the JSON and image before this, so the URI resolves the moment the token exists.
Your token account and the supply
import {
createAssociatedTokenAccountIdempotentInstruction,
createMintToInstruction,
getAssociatedTokenAddressSync,
} from "@solana/spl-token";
const ata = getAssociatedTokenAddressSync(mint.publicKey, payer);
const createAta = createAssociatedTokenAccountIdempotentInstruction(payer, ata, payer, mint.publicKey);
const supply = 1_000_000_000n * 10n ** BigInt(decimals); // 1 billion tokens, in base units
const mintSupply = createMintToInstruction(mint.publicKey, ata, payer, supply);Revoke, and send it all
import { AuthorityType, createSetAuthorityInstruction } from "@solana/spl-token";
const revokeMint = createSetAuthorityInstruction(mint.publicKey, payer, AuthorityType.MintTokens, null);
const revokeFreeze = createSetAuthorityInstruction(mint.publicKey, payer, AuthorityType.FreezeAccount, null);
const signature = await sendInstructions(
connection,
wallet,
[createMintAccount, initMint, createMetadata, createAta, mintSupply, revokeMint, revokeFreeze],
[mint] // the new mint keypair co-signs; the wallet signs as payer and authority
);Seven instructions, one signature from the wallet, atomic: either the token exists with its metadata, supply and revoked authorities, or nothing was created and only the fee was paid. This is the transaction SPL Token Creator builds.
The same mint on Token-2022, with a transfer fee
Extensions change two things: the mint is larger, so you compute its size from the extension list, and each extension's initialise instruction must run before initializeMint2, while the account is still blank.
import {
ExtensionType,
TOKEN_2022_PROGRAM_ID,
createInitializeMint2Instruction,
createInitializeTransferFeeConfigInstruction,
getMintLen,
} from "@solana/spl-token";
const extensions = [ExtensionType.TransferFeeConfig];
const space = getMintLen(extensions);
const createMintAccount = SystemProgram.createAccount({
fromPubkey: payer,
newAccountPubkey: mint.publicKey,
space,
lamports: await connection.getMinimumBalanceForRentExemption(space),
programId: TOKEN_2022_PROGRAM_ID,
});
const initFee = createInitializeTransferFeeConfigInstruction(
mint.publicKey,
payer, // can change the fee later (null to lock it)
payer, // can withdraw withheld fees
100, // 1% in basis points
1_000_000n, // max fee per transfer, base units
TOKEN_2022_PROGRAM_ID
);
const initMint = createInitializeMint2Instruction(mint.publicKey, decimals, payer, null, TOKEN_2022_PROGRAM_ID);
// order: createMintAccount, initFee, initMint — then ATA and mintTo, each with TOKEN_2022_PROGRAM_IDEvery later call takes the program id as its last argument — the ATA derivation, mintTo, transferChecked — because the same helper serves both programs and the address changes with it. Metadata can go in the mint through the MetadataPointer and TokenMetadata extensions instead of Metaplex; Token-2022 Creator does both. Token-2022 extensions covers what each one means for holders.
Reading a mint back
import { getMint, getTokenMetadata } from "@solana/spl-token";
const info = await getMint(connection, mint.publicKey, "confirmed", TOKEN_2022_PROGRAM_ID);
console.log(info.supply, info.decimals, info.mintAuthority, info.freezeAuthority);
// Token-2022 only: metadata stored in the mint
const meta = await getTokenMetadata(connection, mint.publicKey);
console.log(meta?.name, meta?.symbol, meta?.uri);What to remember
- A mint is createAccount (System, rent) + initializeMint2 (Token). The mint keypair co-signs its own creation.
- Metadata is a PDA created in the same transaction, while you still hold the mint authority.
- Supply goes into your ATA with mintTo, in base units. Revokes are setAuthority to null.
- One atomic transaction: seven instructions, one wallet signature.
- Token-2022: size from getMintLen, extension inits before initializeMint2, program id on every later call.