Skip to main content
Solana Token Extensions: Token-2022 Getting Started Guide - Featured image
Smart Contracts

Solana Token Extensions: Token-2022 Getting Started Guide

BUILDERZ

BUILDERZ

Share

In this guide

The decisions and working steps ahead.

    What Token-2022 extensions change, and when to use them

    Token-2022 or classic SPL?

    Set up a Kit project for Token-2022

You ship a badge as a classic token. A week later, people trade it on a marketplace.

The classic token program can't stop that. Token-2022 can.

The non-transferable extension is set when the mint is created. After that, the token program itself rejects every transfer.

Save this guide as your launch checklist. You'll build a badge that can't move.

You'll also build a treasury that demands memos.

Then you'll learn the 5 pitfalls that force a reissue.

What Token-2022 extensions change, and when to use them

Token-2022 is a second token program on Solana.

It does everything the classic program does. It also lets you attach extra rules and data to a mint or a token account. These add-ons are called extensions.

The Solana token extensions docs split them into two groups:

  • Mint extensions apply to every holder. Transfer hooks, transfer fees, metadata pointers, and permanent delegates are all mint extensions.
  • Account extensions apply to one token account. Required memos, immutable owners, and the CPI guard are account extensions.

The same docs warn that most extensions can't be added after an account is set up. So you choose the rules before the mint exists. If you change your mind later, you need a new mint and a migration.

Token-2022 or classic SPL?

Use the classic program for a plain token that needs the widest support across older wallets, bots, and DeFi code. It's the default for good reason.

Pick Token-2022 when the token needs a rule the classic program can't enforce. Good fits look like this:

  • badges, credentials, and passes that must stay in one wallet
  • treasury or exchange accounts that need a memo on every deposit
  • tokens with built-in transfer fees or on-chain metadata
  • regulated or real-world asset tokens that need checks on each transfer

If none of those apply, stay on the classic program. Each extension adds account space, rent, and integration work.

Rent is the quiet cost. A bigger account needs more SOL locked up to stay rent-exempt, and that bill grows with every holder account you create.

Set up a Kit project for Token-2022

This guide uses the Kit SDK with the generated Token-2022 client. These are the current npm releases:

Start from any TypeScript project that runs ES modules, since the snippets use top-level await. Pin the versions. The generated clients move fast, and a minor bump can rename a helper.

Code examplebash
pnpm add @solana/kit@8.5.0 @solana-program/token-2022@0.20.0 @solana-program/system@0.16.0

Every snippet type-checks against these versions. Run them on a test network before you touch real funds.

Kit builds a transaction as plain data. You set a fee payer, a recent block hash, and the instructions. Then you sign and send. This helper wraps that flow, so each later step stays short.

Code exampletypescript
import {
  appendTransactionMessageInstructions,
  assertIsTransactionWithBlockhashLifetime,
  createSolanaRpc,
  createSolanaRpcSubscriptions,
  createTransactionMessage,
  getSignatureFromTransaction,
  pipe,
  sendAndConfirmTransactionFactory,
  setTransactionMessageFeePayerSigner,
  setTransactionMessageLifetimeUsingBlockhash,
  signTransactionMessageWithSigners,
  type Instruction,
  type TransactionSigner,
} from '@solana/kit';

const rpc = createSolanaRpc('https://api.devnet.solana.com');
const rpcSubscriptions = createSolanaRpcSubscriptions('wss://api.devnet.solana.com');
const sendAndConfirm = sendAndConfirmTransactionFactory({ rpc, rpcSubscriptions });

async function send(payer: TransactionSigner, instructions: Instruction[]) {
  const { value: blockhash } = await rpc.getLatestBlockhash().send();
  const message = pipe(
    createTransactionMessage({ version: 0 }),
    (m) => setTransactionMessageFeePayerSigner(payer, m),
    (m) => setTransactionMessageLifetimeUsingBlockhash(blockhash, m),
    (m) => appendTransactionMessageInstructions(instructions, m),
  );
  const signed = await signTransactionMessageWithSigners(message);
  assertIsTransactionWithBlockhashLifetime(signed);
  await sendAndConfirm(signed, { commitment: 'confirmed' });
  return getSignatureFromTransaction(signed);
}

The assert call tells TypeScript the transaction expires with its block hash. The confirmation step needs that, and the code won't compile without it. Every later snippet reuses this helper and the same client.

Create a badge mint that can't be moved

The non-transferable extension sits on the mint. The Token-2022 extension guide calls these "soul-bound" tokens that can't move to anyone else.

Holders can still burn the token and close the account. That's useful when a badge expires or a pass gets revoked.

Creating the mint takes three steps in one transaction. First, make an account big enough for the mint plus its extensions. Next, set up each extension. Last, set up the mint itself.

Code exampletypescript
import { generateKeyPairSigner } from '@solana/kit';
import { getCreateAccountInstruction } from '@solana-program/system';
import {
  TOKEN_2022_PROGRAM_ADDRESS,
  extension,
  getInitializeMintInstruction,
  getMintSize,
  getPreInitializeInstructionsForMintExtensions,
} from '@solana-program/token-2022';

const authority = await generateKeyPairSigner(); // fund this wallet on devnet first
const badgeMint = await generateKeyPairSigner();
const mintExtensions = [extension('NonTransferable', {})];

// Size the account for the base mint plus every extension.
const mintSpace = BigInt(getMintSize(mintExtensions));
const mintRent = await rpc.getMinimumBalanceForRentExemption(mintSpace).send();

await send(authority, [
  getCreateAccountInstruction({
    payer: authority,
    newAccount: badgeMint,
    lamports: mintRent,
    space: mintSpace,
    programAddress: TOKEN_2022_PROGRAM_ADDRESS, // owned by Token-2022, not classic SPL
  }),
  // Extension setup must run before InitializeMint.
  ...getPreInitializeInstructionsForMintExtensions(badgeMint.address, mintExtensions),
  getInitializeMintInstruction({
    mint: badgeMint.address,
    decimals: 0,
    mintAuthority: authority.address,
  }),
]);

The pre-initialize helper returns the right setup step for each extension in your list. It also keeps the order right when you add more extensions later.

Decimals are zero because a badge is whole. Nobody should hold half a credential. Keep the mint authority on a wallet you control, since it's the only key that can issue more badges.

Issue one badge to a holder

Next, create the holder's associated token account and mint one badge into it.

Code exampletypescript
import {
  findAssociatedTokenPda,
  getCreateAssociatedTokenIdempotentInstructionAsync,
  getMintToCheckedInstruction,
} from '@solana-program/token-2022';

const holder = await generateKeyPairSigner();
const [holderAta] = await findAssociatedTokenPda({
  owner: holder.address,
  mint: badgeMint.address,
  tokenProgram: TOKEN_2022_PROGRAM_ADDRESS, // part of the address seeds
});

await send(authority, [
  await getCreateAssociatedTokenIdempotentInstructionAsync({
    payer: authority,
    owner: holder.address,
    mint: badgeMint.address,
  }),
  getMintToCheckedInstruction({
    mint: badgeMint.address,
    token: holderAta,
    mintAuthority: authority,
    amount: 1n,
    decimals: 0,
  }),
]);

Any later transfer of that badge fails inside the token program. You don't write or audit any custom code to enforce it.

Require a memo on every deposit

The required memo extension works the other way round. It lives on a token account, not the mint.

The memo transfer docs say a memo instruction must come right before any incoming transfer to that account. Outgoing transfers don't need one.

That fits a treasury or exchange deposit account. Every deposit must carry a reference your back office can match.

Code exampletypescript
import {
  getInitializeAccount3Instruction,
  getPostInitializeInstructionsForTokenExtensions,
  getTokenSize,
} from '@solana-program/token-2022';

const usdMint = await generateKeyPairSigner(); // stands in for a Token-2022 mint you created earlier
const treasury = await generateKeyPairSigner();
const accountExtensions = [extension('MemoTransfer', { requireIncomingTransferMemos: true })];

const accountSpace = BigInt(getTokenSize(accountExtensions));
const accountRent = await rpc.getMinimumBalanceForRentExemption(accountSpace).send();

await send(authority, [
  getCreateAccountInstruction({
    payer: authority,
    newAccount: treasury,
    lamports: accountRent,
    space: accountSpace,
    programAddress: TOKEN_2022_PROGRAM_ADDRESS,
  }),
  getInitializeAccount3Instruction({
    account: treasury.address,
    mint: usdMint.address,
    owner: authority.address,
  }),
  // EnableMemoTransfers runs after the account exists, signed by the owner.
  ...getPostInitializeInstructionsForTokenExtensions(treasury.address, authority, accountExtensions),
]);

Notice that the order flips here. Mint extensions are set up before the mint. This account rule is switched on after the account exists, and the owner must sign it.

You can also turn the rule on for an account that already holds tokens. The extension guide notes the account may need more space first, so add a resize step.

What senders must change

Anyone paying into this account now needs two steps in one transaction. The first is a memo program call with your reference, such as an invoice ID. The second is the transfer itself, placed right after it. Tell your payers before you switch the rule on, or their deposits will fail.

The 5 pitfalls that break Token-2022 launches

Each of these is cheap to catch in testing. Each one is costly to find after launch.

1. The wrong program in the token account address

An associated token address comes from three inputs: the owner, the token program, and the mint. If your code passes the classic program, it gets a different address. The account looks missing even though the tokens are there.

The Kit helper makes you pass the token program, which helps. Older code that hard-codes the classic program is where this bites. Check every place your app, indexer, and backend work out a token address.

2. Extension order

Mint extensions must be set up before the mint itself. Put them after, and the transaction fails. Use the pre-initialize helper instead of ordering steps by hand.

3. Account size

The mint account must be sized for its extensions up front. Pass the same extension list to the size helper and the setup helper, so the two can't drift apart.

4. Combinations the program rejects

Some extensions can't share a mint. The Token-2022 program rejects these pairs when you create the mint:

  • a scaled UI amount together with interest-bearing config
  • non-transferable with confidential transfers, unless confidential mint and burn is also on
  • transfer fees with confidential transfers, unless confidential transfer fees are also on

5. Wallet and app support

Not every wallet, explorer, or DeFi app handles every extension. Treat support as something you test. Send your test mint through the exact wallets and apps your users will touch.

FAQ

Can I add a token extension to an existing mint?

Usually not. Most mint extensions must be set up before the mint is created, so you plan them up front. Some account extensions, like required memos, can be turned on later for an existing token account.

Do Token-2022 tokens work with the classic SPL token program?

No. They belong to a different program, and a Token-2022 mint only accepts Token-2022 instructions. Its token account addresses also use the Token-2022 program in their seeds, so your code must pass the right program everywhere.

Can a non-transferable token be burned?

Yes. The Token-2022 extension guide says the holder can still burn the token and close the account. What they can't do is send it to another wallet.

Should I use the Kit SDK or the older SPL token library?

Use the Kit SDK with the generated Token-2022 client for new code. It's built for the current program and ships helpers for extension order and sizing. Keep the older library for codebases that already run on it.

When should I write a transfer hook instead?

Write a transfer hook when the rule needs your own logic, like an allowlist or a per-wallet limit. The non-transferable and memo extensions are fixed rules that need no extra program. Our guide to Token-2022 transfer hooks shows how to build one in Anchor.

Get your Token-2022 design reviewed

Our first published Solana build shipped in 2021. Our historical TicketSensei build paired an on-chain ticketing contract with a full backend. Mint design and launch work like this sits in our Solana development service. If you're still choosing extensions, an Architecture Sprint ($3K-$5K) can scope it.

Planning a mint with extensions? Join a call about your Token-2022 design.

Continue reading

Want to turn your idea into reality?

Book a 20-minute intro call, or send a short project brief. We reply within one business day.