SOLSTACKAcademy

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.

src/solstack/lib/tokens.js · buildCreateSplToken, trimmedTypeScript
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

TypeScript
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

TypeScript
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

TypeScript
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.

TypeScript
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_ID

Every 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

TypeScript
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.

Try it

0 of 32 lessons done