Skip to main content
TLDR:
  • The Token Extensions Program (Token-2022) is a superset of the original SPL Token Program with optional extensions you enable at mint creation time.
  • Extensions add features like transfer fees, on-chain metadata, confidential transfers, non-transferable (soulbound) tokens, permanent delegates, and transfer hooks.
  • Token-2022 tokens work with the same RPC methods (getTokenAccountsByOwner, getTokenSupply, etc.) but require the Token-2022 program ID for filtering.
  • Most extensions cannot be added after mint creation — plan your token design upfront.

Why Token Extensions matter

The original SPL Token Program covers basic mint, transfer, and burn operations. But real-world token use cases need more:
  • Transfer fees — protocol-level fee on every transfer, collected automatically on-chain
  • Confidential transfers — ZK-encrypted balances and transfer amounts for privacy
  • On-chain metadata — store token name, symbol, and URI directly on the mint account (no Metaplex dependency)
  • Non-transferable tokens — soulbound credentials, achievements, identity tokens
  • Permanent delegate — compliance-oriented freeze/clawback capability
  • Transfer hooks — programmable logic that runs on every transfer (royalties, allow-lists, custom validation)
The Token-2022 program ID is TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb.

Get your own node endpoint today

Start for free and get your app to production levels immediately. No credit card required.You can sign up with your GitHub, X, Google, or Microsoft account.

Available extensions

Extensions are enabled at mint or token account creation time. Most cannot be added later.

Mint extensions

ExtensionWhat it does
TransferFeeConfigAutomatic fee on every transfer, collected in withheld accounts
MintCloseAuthorityAllows closing the mint account to reclaim rent
InterestBearingConfigTokens accrue interest over time (UI display, not actual minting)
PermanentDelegateAn authority that can transfer or burn tokens from any account
NonTransferableTokens cannot be transferred (soulbound)
TransferHookCalls a custom program on every transfer (CPI-based)
MetadataPointer + TokenMetadataOn-chain metadata stored directly on the mint
GroupPointer + TokenGroupCollection-like grouping for tokens
ConfidentialTransferMintEnables ZK-encrypted balances and transfers
DefaultAccountStateNew token accounts start as frozen (require explicit thaw)
PausableAdmin can pause all transfers globally
ScaledUiAmountRebasing token display (like stETH)

Account extensions

ExtensionWhat it does
ImmutableOwnerToken account owner cannot be changed (enabled by default for ATAs)
MemoTransferInbound transfers require a memo
CpiGuardBlocks certain token operations when invoked via CPI

Create a token with extensions

This example creates a mint with transfer fees and on-chain metadata.

Prerequisites

npm install @solana/web3.js @solana/spl-token @solana/spl-token-metadata bs58 dotenv

Code

import {
  Connection,
  Keypair,
  SystemProgram,
  Transaction,
  sendAndConfirmTransaction,
} from "@solana/web3.js";
import {
  ExtensionType,
  TOKEN_2022_PROGRAM_ID,
  createInitializeMintInstruction,
  createInitializeTransferFeeConfigInstruction,
  getMintLen,
  createMintToInstruction,
  createAssociatedTokenAccountInstruction,
  getAssociatedTokenAddressSync,
} from "@solana/spl-token";
import {
  createInitializeInstruction,
  createInitializeMetadataPointerInstruction,
  pack,
  TokenMetadata,
  TYPE_SIZE,
  LENGTH_SIZE,
} from "@solana/spl-token-metadata";
import bs58 from "bs58";
import "dotenv/config";

const connection = new Connection(process.env.SOLANA_RPC!);
const payer = Keypair.fromSecretKey(bs58.decode(process.env.PRIVATE_KEY!));
const mintKeypair = Keypair.generate();
const mint = mintKeypair.publicKey;

const decimals = 6;
const transferFeeBasisPoints = 100; // 1%
const maxFee = BigInt(1_000_000); // 1 token max fee

// Metadata for the token
const metadata: TokenMetadata = {
  mint: mint,
  name: "My Hackathon Token",
  symbol: "HACK",
  uri: "https://example.com/metadata.json",
  additionalMetadata: [],
};

async function createTokenWithExtensions() {
  // Calculate space needed for mint + extensions
  const extensions = [ExtensionType.TransferFeeConfig, ExtensionType.MetadataPointer];
  const mintLen = getMintLen(extensions);
  const metadataLen = TYPE_SIZE + LENGTH_SIZE + pack(metadata).length;
  const totalLen = mintLen + metadataLen;

  const lamports = await connection.getMinimumBalanceForRentExemption(totalLen);

  const transaction = new Transaction().add(
    // Create account
    SystemProgram.createAccount({
      fromPubkey: payer.publicKey,
      newAccountPubkey: mint,
      space: mintLen,
      lamports,
      programId: TOKEN_2022_PROGRAM_ID,
    }),
    // Initialize metadata pointer (points to the mint itself)
    createInitializeMetadataPointerInstruction(
      mint,
      payer.publicKey, // metadata pointer authority
      mint, // metadata account (self-referencing)
      TOKEN_2022_PROGRAM_ID
    ),
    // Initialize transfer fee extension
    createInitializeTransferFeeConfigInstruction(
      mint,
      payer.publicKey, // transfer fee config authority
      payer.publicKey, // withdraw withheld authority
      transferFeeBasisPoints,
      maxFee,
      TOKEN_2022_PROGRAM_ID
    ),
    // Initialize mint
    createInitializeMintInstruction(
      mint,
      decimals,
      payer.publicKey,
      null, // no freeze authority
      TOKEN_2022_PROGRAM_ID
    ),
    // Initialize on-chain metadata
    createInitializeInstruction({
      programId: TOKEN_2022_PROGRAM_ID,
      mint: mint,
      metadata: mint, // metadata stored on the mint itself
      name: metadata.name,
      symbol: metadata.symbol,
      uri: metadata.uri,
      mintAuthority: payer.publicKey,
      updateAuthority: payer.publicKey,
    })
  );

  const sig = await sendAndConfirmTransaction(connection, transaction, [
    payer,
    mintKeypair,
  ]);
  console.log("Token created:", mint.toBase58());
  console.log("Transaction:", sig);
}

createTokenWithExtensions();

Query Token-2022 accounts via RPC

When querying Token-2022 accounts, use the Token-2022 program ID in the programId filter:
curl -X POST YOUR_CHAINSTACK_ENDPOINT -H "Content-Type: application/json" -d '{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "getTokenAccountsByOwner",
  "params": [
    "OWNER_PUBKEY",
    {
      "programId": "TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb"
    },
    {
      "encoding": "jsonParsed"
    }
  ]
}'
The jsonParsed response for Token-2022 accounts includes an extensions array showing the active extensions and their configuration. This is the same getTokenAccountsByOwner method — the only difference is the program ID filter.

Key considerations

  • Plan extensions at creation — most extensions cannot be added after the mint is initialized.
  • Incompatible extensions — some combinations don’t work together (e.g., NonTransferable + TransferFeeConfig).
  • Rent costs — extensions increase account size and therefore rent. Calculate space with getMintLen([...extensions]).
  • Wallet support — major wallets (Phantom, Backpack, Solflare) support Token-2022 tokens. Some older wallets may not.
  • DEX support — Jupiter, Raydium, and Orca support Token-2022 tokens with most extensions.

Additional resources

Last modified on April 16, 2026