One command for Claude Code, Cursor, Codex, Windsurf and other agents — installs from the upstream repo (current HEAD).
Drop the mirrored SKILL.md into your skills directory. Global (all projects):
Or scoped to the current project:
Companion files (8) are hosted on this domain too, so relative links resolve. For the full offline bundle use the CLI or Git tab.
The skill is mirrored as plain markdown — tell your agent:
Or have the skills CLI generate a one-shot prompt (pipe it straight into your agent):
Or pull the raw file into a terminal session:
Copy the skill directory with all companion files straight from GitHub:
Skill contents
Open .mdToken Extensions (Token-2022)
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