Token Extensions (Token-2022)
SkillDev toolsToken-2022 (Token Extensions) on Solana. Pick, combine and create mint and account extensions with the spl-token CLI, @solana/kit or Anchor, and integrate extension mints. Use for transfer fees, hooks, metadata, groups, pausable, permanent delegate, soulbound, interest-bearing, scaled UI or confidential tokens.
Use Token Extensions (Token-2022) in Claude, ChatGPT or Ahel Desktop
Free. Sign in, add Token Extensions (Token-2022) and connect your AI. About a minute.
Also: Claude Code · Cursor · Codex
Then ask your AI: use the Token Extensions (Token-2022) skill
Details
Instructions available. Your AI can read the instructions. Execution depends on the setup they require.
Account requirements not reviewed. Check the skill instructions before use; Ahel provides instructions and does not run this skill.
No other account needed.
Add Ahel to your AI once: Claude, ChatGPT, Cursor, Claude Code or Codex. Then ask it to use this.
What this skill tells your AI
The instructions your AI receives, as published by solanabr/ai-kit in .claude/skills/token-extensions/SKILL.md and read by Ahel’s review.
Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb. Checked on 2026-09-30 against the mainnet program (spl-token-2022 v11.0.0, per its verified build), spl-token-2022-interface 3.1.2, spl-token-cli 5.6.1, @solana-program/token-2022 0.19.0 on @solana/kit 8, and anchor-lang / anchor-spl 1.2.0. Newer releases may rename things; check before relying on a name from memory.
Links into the kit's ext/ directory, here and in the references, need its full install; in a plugin install, read the same paths in solana-foundation/solana-dev-skill.
Pick the extensions
Fixed-size mint extensions can only be set before InitializeMint, so pick them up front. TokenMetadata, TokenGroup and TokenGroupMember can be initialized later, but only if their pointer was set at creation. Venues accept different sets; Orca's rules are in programs.
| Extension (mint) | Use it for | Authority | Token accounts get | Read |
|---|---|---|---|---|
| TransferFeeConfig | A fee withheld on every transfer | fee config, withdraw withheld | TransferFeeAmount | fees |
| TransferHook | Your program runs on every transfer | hook authority (sets the program id) | TransferHookAccount | transfer-hooks |
| MetadataPointer + TokenMetadata | Name, symbol, URI and fields stored on the mint | pointer authority, metadata update authority | metadata-groups | |
| GroupPointer + TokenGroup | A collection parent mint | pointer authority, group update authority | metadata-groups | |
| GroupMemberPointer + TokenGroupMember | A mint inside a collection | pointer authority; group update authority co-signs | metadata-groups | |
| PermanentDelegate | Issuer can transfer or burn from any account | permanent delegate | issuer-controls | |
| DefaultAccountState | New accounts start frozen (KYC gating) | freeze authority | issuer-controls | |
| Pausable | Stop transfers, mints and burns | pause authority | PausableAccount | issuer-controls |
| PermissionedBurn | Burning needs an extra authority | permissioned-burn authority | issuer-controls | |
| NonTransferable | Soulbound: holders can't transfer | none | NonTransferableAccount, ImmutableOwner | issuer-controls |
| MintCloseAuthority | Close the mint once supply is 0 | close authority | issuer-controls | |
| InterestBearingConfig | Displayed amount accrues interest | rate authority | display-amounts | |
| ScaledUiAmount | Displayed amount = raw × multiplier | multiplier authority | display-amounts | |
| ConfidentialTransferMint (+ ConfidentialTransferFeeConfig, ConfidentialMintBurn) | Encrypted balances and amounts | confidential authority, auditor key | ConfidentialTransferAccount, opt-in per account | confidential |
Holder-side account extensions (ImmutableOwner, MemoTransfer, CpiGuard), account sizing and Reallocate: account-extensions. Accepting any Token-2022 mint in a program, and CPIs from Anchor: programs.
Combinations
| Combination | Result |
|---|---|
| ConfidentialTransferFeeConfig without both TransferFeeConfig and ConfidentialTransferMint | Rejected at InitializeMint: InvalidExtensionCombination |
| TransferFeeConfig + ConfidentialTransferMint without ConfidentialTransferFeeConfig | Rejected: InvalidExtensionCombination |
| ConfidentialMintBurn without ConfidentialTransferMint | Rejected: InvalidExtensionCombination |
| NonTransferable + ConfidentialTransferMint without ConfidentialMintBurn | Rejected: InvalidExtensionCombination |
| InterestBearingConfig + ScaledUiAmount | Rejected: InvalidExtensionCombination |
DefaultAccountState Frozen without a freeze authority | Rejected: MintCannotFreeze |
| NonTransferable + TransferFeeConfig or TransferHook | Accepted, but pointless: every transfer fails with NonTransferable |
| NonTransferable + PermanentDelegate | The delegate can burn but not transfer |
| TransferHook + ConfidentialTransferMint | Confidential transfers call the hook with amount = u64::MAX |
| Pausable, while paused | TransferChecked, MintTo and burns fail with MintPaused |
Tooling gaps: anchor-spl 1.2.0 has no helpers for ScaledUiAmount, PermissionedBurn or the confidential extensions (it builds on spl-token-2022-interface 2.x, which has no PermissionedBurn), and spl-token-cli 5.6.1 can't initialize ConfidentialMintBurn.
spl-token-cli 5.6.1 can't act for a multisig extension authority. set-interest-rate, set-transfer-fee, set-transfer-hook, initialize-metadata, update-metadata, initialize-group, update-group-max-size, initialize-member and update-confidential-transfer-settings reject --multisig-signer. pause, resume and update-ui-amount-multiplier accept it but ignore it and send an under-signed transaction. Build those instructions with Kit or Rust instead.
Confidential transfers need the ZK ElGamal Proof program enabled on the cluster (on mainnet since epoch 982, and on devnet), a per-account opt-in (Reallocate, then ConfigureAccount with a proof), and an owner who applies pending balances. Decisions and what is out of date in the linked Rust walkthrough: confidential.
Create a mint
One transaction, in this order:
- Create the account, owned by Token-2022, with space for the fixed-size extensions only:
ExtensionType::try_calculate_account_len::<Mint>(&types)(Rust),getMintLen(types)(web3.js 1.x), or Kit'sgetMintSizeover the list without TokenMetadata, TokenGroup and TokenGroupMember (it counts whatever it is given), with rent fromgetMintSizeover the full list.InitializeMintrejects any other length (InvalidAccountData) and a mint below the rent-exempt minimum (NotRentExempt). - Each extension's initialize instruction. On an initialized mint these fail with
AlreadyInUse, so fixed-size extensions can't be added later. InitializeMint2(orInitializeMint).- TokenMetadata, TokenGroup and TokenGroupMember. They grow the mint but move no lamports, so fund the mint for its final size in step 1 (Kit's
createMintdoes), or transfer the difference to the mint before the initialize, in an earlier instruction of the same transaction (the CLI does) or in your program before the CPI.
Kit: the plugin's createMint does the sizing, funding and ordering. It doesn't initialize TokenGroupMember, doesn't write additionalMetadata (it only pays rent for it), and skips TokenMetadata when updateAuthority is null (metadata-groups). client is a Kit 8 client with an RPC, a payer and transaction planning and sending (Kit plugins).
import { generateKeyPairSigner } from '@solana/kit';
import { extension, token2022Program } from '@solana-program/token-2022';
const token = client.use(token2022Program());
const mint = await generateKeyPairSigner();
const authority = client.payer.address;
await token.token2022.instructions
.createMint({
newMint: mint,
decimals: 6,
mintAuthority: client.payer,
extensions: [
extension('MetadataPointer', { authority, metadataAddress: mint.address }),
extension('TokenMetadata', {
updateAuthority: authority, mint: mint.address,
name: 'Example', symbol: 'EXM', uri: 'https://example.com/exm.json',
additionalMetadata: new Map(),
}),
],
})
.sendTransaction();
Without the plugin, compose the same steps (space from the filtered list, rent from the full one, as createMint does) with getMintSize, getPreInitializeInstructionsForMintExtensions, getInitializeMintInstruction and getPostInitializeInstructionsForMintExtensions, or call getCreateMintInstructionPlan(client, input).
CLI: spl-token --program-2022 create-token --decimals 6 --enable-metadata then spl-token initialize-metadata <MINT> Example EXM https://example.com/exm.json. After creation the CLI infers the program from the account's owner, so later commands don't need --program-2022, except with --sign-only, where it can't look the account up and defaults to classic Token.
Anchor 1.2.0: init on an InterfaceAccount<'info, Mint> accepts extensions::metadata_pointer::{authority, metadata_address}, extensions::group_pointer::{authority, group_address}, extensions::group_member_pointer::{authority, member_address}, extensions::transfer_hook::{authority, program_id}, extensions::close_authority::authority, extensions::permanent_delegate::delegate and extensions::pausable::authority. The same constraints on an existing mint check the extension's values. TokenMetadata, TokenGroup and TokenGroupMember are CPIs after init (metadata-groups); the other extensions need a manual create-and-initialize sequence (programs).
Token accounts and transfers
- Associated token accounts derive from the token program id. Pass the Token-2022 id (
findAssociatedTokenPda({ owner, mint, tokenProgram }),getAssociatedTokenAddressSync(mint, owner, false, TOKEN_2022_PROGRAM_ID)), or you get the address of an account that doesn't exist. web3.js'screateAssociatedTokenAccountIdempotentInstructionWithDerivationderives with the classic program id even when you pass Token-2022; derive the address yourself. - Token accounts need the extensions their mint requires (table above). Associated token accounts and Anchor's
initget them. For an account you create yourself, Kit'sgetTokenSizecounts only the extensions you pass it: account-extensions. - Transfer with
TransferChecked. PlainTransferfails withMintRequiredForTransferwhen the source account has TransferFeeAmount, TransferHookAccount or PausableAccount, because the program needs the mint to charge the fee, call the hook or check the pause. - With a fee, the recipient gets
amount - fee: credit the balance delta, not the argument. The fee stays withheld in the recipient's account until anyone harvests it to the mint or the withdraw authority withdraws it; an account holding withheld fees can't close (fees). - With a hook, the hook program's ExtraAccountMetaList PDA (seeds
["extra-account-metas", mint]) must exist before the first transfer, and every transfer appends the hook program, that PDA and the extra accounts it lists (Kit:getTransferCheckedWithTransferHookInstructionAsync). - A
TransferCheckedto the same account moves nothing, charges no fee and skips the hook. Confidential transfers have no such shortcut and call the hook. - In a program,
anchor_spl::token_interface::transfer_checkedpasses only the four base accounts, so it can't move a hook mint: transfer-hooks. - Destinations with MemoTransfer enabled need a memo instruction right before the transfer. Owners with CpiGuard enabled can't sign transfers inside a CPI.
Reading an unknown mint
- Kit:
fetchMint(rpc, address)thenmint.data.extensions, anOptionof an array tagged by__kind. web3.js 1.x:getExtensionTypes(mint.tlvData)on the result ofgetMint(connection, address, undefined, TOKEN_2022_PROGRAM_ID). Rust:StateWithExtensions::<Mint>::unpack(&data)?thenget_extension_types()orget_extension::<T>(); Anchor:anchor_spl::token_interface::get_mint_extension_data::<T>(&account_info). - Match on each tool's own names. Kit's
__kindisScaledUiAmountConfig,PausableConfigandConfidentialTransferFeewhere Rust says ScaledUiAmount, Pausable and ConfidentialTransferFeeConfig; web3.js usesScaledUiAmountConfigandPausableConfig. Under anchor-spl 1.2.0 (interface 2.x),get_extension_types()fails withInvalidAccountDataon a PermissionedBurn mint, whileget_extension::<T>()still works. - Allow-list the extensions you support. PermanentDelegate, TransferHook, Pausable, DefaultAccountState, NonTransferable and TransferFeeConfig each change what your program or UI can rely on; the attack side is in security.md, Token-2022 section.
- Show balances with the mint's UI conversion when InterestBearingConfig or ScaledUiAmount is present: display-amounts.
Related
- kit/programs/token-2022.md: the Kit client basics. Its "Extension Initialization Order" puts every extension before the mint initialize; TokenMetadata, TokenGroup and TokenGroupMember go after it, as in Create a mint. confidential-transfers.md: the Rust confidential flow; confidential lists what in it is out of date. testing.md: LiteSVM and Mollusk.
- NFTs and collections: the skills hub also routes to the Metaplex skill (install first:
bash .claude/bin/skills.sh add metaplex). - Official guides: https://solana.com/docs/tokens/extensions
Signals
- GitHub stars
- 106
- Forks
- 59
- Last commit
- Oct 2026
Others that do the same job
Advanced
- Item type
- skill
- Key
token-extensions- Source
- github.com/solanabr/ai-kit
Related picks
Skill · thedaviddias
The pick for JavaScriptmodern-javascript-patterns
Skill · wshobson
The pick for JavaScriptomh-rust
Skill · rlaope
The pick for Rustrust-async-task-design
Skill · hashgraph-online
The pick for Rustsetup-ts-deep-modules
Skill · mattpocock
The pick for TypeScripttypescript-pro
Skill · jeffallan
The pick for TypeScript