---
title: Non-Transferable Tokens
description:
  Create a mint whose tokens can be minted, burned, and closed out, but cannot
  be transferred between token accounts.
url: /docs/tokens/extensions/non-transferrable-tokens
type: tutorial
prerequisites:
  - /docs/tokens/basics/create-mint
  - /docs/tokens/basics/create-token-account
  - /docs/tokens/basics/mint-tokens
related:
  - /docs/tokens/extensions/immutable-owner
  - /docs/tokens/basics/burn-tokens
  - /docs/tokens/basics/close-account
  - /docs/tokens/extensions
---

## What Are Non-Transferable Tokens?

The Token Extension Program's _rs`NonTransferable`_ mint extension makes every
token account for that mint non-transferable. After tokens are minted, token
holders cannot move them to another token account with _rs`Transfer`_ or
_rs`TransferChecked`_.

This pattern is useful for assets that should stay attached to one wallet.

Non-transferable tokens can still be:

- Minted by the mint authority
- Burned by the token account owner or an authorized delegate
- Token accounts can be closed after the balance reaches zero

<Callout type="info" title="Token Account Extensions">
  When a token account is initialized for a mint with _rs`NonTransferable`_, the
  token account is initialized with _rs`NonTransferableAccount`_ and
  _rs`ImmutableOwner`_. When the token account is created through the
  [Associated Token
  Program](/docs/tokens/basics/create-token-account#how-to-create-an-associated-token-account),
  the required account size is calculated and the token account is created with
  the required size and rent-exempt lamports.
</Callout>

## How to Create a Non-Transferable Mint

To create a non-transferable mint:

1. Calculate the mint account size and rent needed for the mint and the
   _rs`NonTransferable`_ extension.
2. Create the mint account with _rs`CreateAccount`_, initialize
   _rs`NonTransferable`_, and initialize the mint with _rs`InitializeMint`_.
3. Create token accounts for the mint. _rs`NonTransferableAccount`_ and
   _rs`ImmutableOwner`_ are automatically enabled for token accounts.
4. _rs`Transfer`_ and _rs`TransferChecked`_ fail with
   _rs`TokenError::NonTransferable`_.

<ScrollyCoding>

## !!steps Calculate account size

Calculate the mint account size for the base mint plus the _rs`NonTransferable`_
extension. This is the size used in _rs`CreateAccount`_.

<CodePlaceholder title="Example" />

```ts !! title="Example"
const client = await createClient()
  .use(generatedPayer())
  .use(
    solanaRpc({
      rpcUrl: "http://localhost:8899",
      rpcSubscriptionsUrl: "ws://localhost:8900"
    })
  )
  .use(rpcAirdrop())
  .use(airdropPayer(lamports(1_000_000_000n)));

const mint = await generateKeyPairSigner();
const recipient = await generateKeyPairSigner();

// !focus(1:1)
const nonTransferableExtension = extension("NonTransferable", {});

// !focus(1:1)
const mintSpace = BigInt(getMintSize([nonTransferableExtension]));
```

## !!steps Calculate rent

Calculate rent using the size needed for the mint plus the _rs`NonTransferable`_
extension.

<CodePlaceholder title="Example" />

```ts !! title="Example"
const client = await createClient()
  .use(generatedPayer())
  .use(
    solanaRpc({
      rpcUrl: "http://localhost:8899",
      rpcSubscriptionsUrl: "ws://localhost:8900"
    })
  )
  .use(rpcAirdrop())
  .use(airdropPayer(lamports(1_000_000_000n)));

const mint = await generateKeyPairSigner();
const recipient = await generateKeyPairSigner();

const nonTransferableExtension = extension("NonTransferable", {});

const mintSpace = BigInt(getMintSize([nonTransferableExtension]));

// !focus(1:3)
const mintRent = await client.rpc
  .getMinimumBalanceForRentExemption(mintSpace)
  .send();
```

## !!steps Create the mint account

Create the mint account with the calculated space and lamports.

<CodePlaceholder title="Example" />

```ts !! title="Example"
const client = await createClient()
  .use(generatedPayer())
  .use(
    solanaRpc({
      rpcUrl: "http://localhost:8899",
      rpcSubscriptionsUrl: "ws://localhost:8900"
    })
  )
  .use(rpcAirdrop())
  .use(airdropPayer(lamports(1_000_000_000n)));

const mint = await generateKeyPairSigner();
const recipient = await generateKeyPairSigner();

const nonTransferableExtension = extension("NonTransferable", {});

const mintSpace = BigInt(getMintSize([nonTransferableExtension]));

const mintRent = await client.rpc
  .getMinimumBalanceForRentExemption(mintSpace)
  .send();

// !focus(1:9)
await client.sendTransaction([
  getCreateAccountInstruction({
    payer: client.payer,
    newAccount: mint,
    lamports: mintRent,
    space: mintSpace,
    programAddress: TOKEN_2022_PROGRAM_ADDRESS
  })
]);
```

## !!steps Initialize NonTransferable

Initialize the _rs`NonTransferable`_ extension on the mint.

<CodePlaceholder title="Example" />

```ts !! title="Example"
const client = await createClient()
  .use(generatedPayer())
  .use(
    solanaRpc({
      rpcUrl: "http://localhost:8899",
      rpcSubscriptionsUrl: "ws://localhost:8900"
    })
  )
  .use(rpcAirdrop())
  .use(airdropPayer(lamports(1_000_000_000n)));

const mint = await generateKeyPairSigner();
const recipient = await generateKeyPairSigner();

const nonTransferableExtension = extension("NonTransferable", {});

const mintSpace = BigInt(getMintSize([nonTransferableExtension]));

const mintRent = await client.rpc
  .getMinimumBalanceForRentExemption(mintSpace)
  .send();

await client.sendTransaction([
  getCreateAccountInstruction({
    payer: client.payer,
    newAccount: mint,
    lamports: mintRent,
    space: mintSpace,
    programAddress: TOKEN_2022_PROGRAM_ADDRESS
  }),
  // !focus(1:3)
  getInitializeNonTransferableMintInstruction({
    mint: mint.address
  })
]);
```

## !!steps Initialize the mint

Initialize the mint with _rs`InitializeMint`_ in the same transaction.

<CodePlaceholder title="Example" />

```ts !! title="Example"
const client = await createClient()
  .use(generatedPayer())
  .use(
    solanaRpc({
      rpcUrl: "http://localhost:8899",
      rpcSubscriptionsUrl: "ws://localhost:8900"
    })
  )
  .use(rpcAirdrop())
  .use(airdropPayer(lamports(1_000_000_000n)));

const mint = await generateKeyPairSigner();
const recipient = await generateKeyPairSigner();

const nonTransferableExtension = extension("NonTransferable", {});

const mintSpace = BigInt(getMintSize([nonTransferableExtension]));

const mintRent = await client.rpc
  .getMinimumBalanceForRentExemption(mintSpace)
  .send();

await client.sendTransaction([
  getCreateAccountInstruction({
    payer: client.payer,
    newAccount: mint,
    lamports: mintRent,
    space: mintSpace,
    programAddress: TOKEN_2022_PROGRAM_ADDRESS
  }),
  getInitializeNonTransferableMintInstruction({
    mint: mint.address
  }),
  // !focus(1:6)
  getInitializeMintInstruction({
    mint: mint.address,
    decimals: 0,
    mintAuthority: client.payer.address,
    freezeAuthority: client.payer.address
  })
]);
```

</ScrollyCoding>

<Callout type="info" title="Instruction Order">
  _rs`InitializeNonTransferableMint`_ must come before _rs`InitializeMint`_.
  _rs`CreateAccount`_, _rs`InitializeNonTransferableMint`_, and
  _rs`InitializeMint`_ must be included in the same transaction.
</Callout>

### Source Reference

| Item                                           | Description                                                                                                                                                                                                                     | Source                                                                                                                            |
| ---------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- |
| _rs`NonTransferable`_                          | Mint extension that marks tokens from the mint as non-transferable.                                                                                                                                                             | [Source](https://github.com/solana-program/token-2022/blob/program%40v10.0.0/interface/src/extension/non_transferable.rs#L8-L13)  |
| _rs`NonTransferableAccount`_                   | Token account extension added to token accounts for non-transferable mints.                                                                                                                                                     | [Source](https://github.com/solana-program/token-2022/blob/program%40v10.0.0/interface/src/extension/non_transferable.rs#L15-L21) |
| _rs`ImmutableOwner`_                           | Token account extension that prevents ownership changes and is required for non-transferable token accounts.                                                                                                                    | [Source](https://github.com/solana-program/token-2022/blob/program%40v10.0.0/interface/src/extension/immutable_owner.rs#L8-L16)   |
| _rs`InitializeNonTransferableMint`_            | Instruction that initializes the mint-level non-transferable extension before _rs`InitializeMint`_.                                                                                                                             | [Source](https://github.com/solana-program/token-2022/blob/program%40v10.0.0/interface/src/instruction.rs#L642-L653)              |
| _rs`process_initialize_non_transferable_mint`_ | Processor logic that initializes the _rs`NonTransferable`_ mint extension on an uninitialized mint.                                                                                                                             | [Source](https://github.com/solana-program/token-2022/blob/program%40v10.0.0/program/src/processor.rs#L1646-L1655)                |
| _rs`get_required_init_account_extensions`_     | Used to automatically add token account extensions when token accounts are initialized based on extensions enabled on the mint. For _rs`NonTransferable`_ mints, it adds _rs`NonTransferableAccount`_ and _rs`ImmutableOwner`_. | [Source](https://github.com/solana-program/token-2022/blob/program%40v10.0.0/interface/src/extension/mod.rs#L1304-L1307)          |

### Typescript

The `Kit` example below uses the generated instructions directly. Legacy
examples using `@solana/web3.js` are included for reference.

#### Kit

<CodeTabs storage="token-ts-kit" flags="r">

```ts !! title="Instructions"
import { lamports, createClient, generateKeyPairSigner } from "@solana/kit";
import { solanaRpc, rpcAirdrop } from "@solana/kit-plugin-rpc";
import { generatedPayer, airdropPayer } from "@solana/kit-plugin-signer";
import { getCreateAccountInstruction } from "@solana-program/system";
import {
  extension,
  fetchMint,
  fetchToken,
  findAssociatedTokenPda,
  getCreateAssociatedTokenInstructionAsync,
  getInitializeMintInstruction,
  getInitializeNonTransferableMintInstruction,
  getMintSize,
  getMintToCheckedInstruction,
  getTransferCheckedInstruction,
  TOKEN_2022_PROGRAM_ADDRESS
} from "@solana-program/token-2022";

const client = await createClient()
  .use(generatedPayer())
  .use(
    solanaRpc({
      rpcUrl: "http://localhost:8899",
      rpcSubscriptionsUrl: "ws://localhost:8900"
    })
  )
  .use(rpcAirdrop())
  .use(airdropPayer(lamports(1_000_000_000n)));

const mint = await generateKeyPairSigner();
const recipient = await generateKeyPairSigner();

const nonTransferableExtension = extension("NonTransferable", {});
const mintSpace = BigInt(getMintSize([nonTransferableExtension]));
const mintRent = await client.rpc
  .getMinimumBalanceForRentExemption(mintSpace)
  .send();

await client.sendTransaction([
  getCreateAccountInstruction({
    payer: client.payer,
    newAccount: mint,
    lamports: mintRent,
    space: mintSpace,
    programAddress: TOKEN_2022_PROGRAM_ADDRESS
  }),
  // !mark(1:3)
  getInitializeNonTransferableMintInstruction({
    mint: mint.address // Mint account that stores the NonTransferable extension.
  }),
  getInitializeMintInstruction({
    mint: mint.address,
    decimals: 0,
    mintAuthority: client.payer.address,
    freezeAuthority: client.payer.address
  })
]);

const [sourceToken] = await findAssociatedTokenPda({
  mint: mint.address,
  owner: client.payer.address,
  tokenProgram: TOKEN_2022_PROGRAM_ADDRESS
});

const [destinationToken] = await findAssociatedTokenPda({
  mint: mint.address,
  owner: recipient.address,
  tokenProgram: TOKEN_2022_PROGRAM_ADDRESS
});

await client.sendTransaction([
  await getCreateAssociatedTokenInstructionAsync({
    payer: client.payer,
    mint: mint.address,
    owner: client.payer.address
  }),
  await getCreateAssociatedTokenInstructionAsync({
    payer: client.payer,
    mint: mint.address,
    owner: recipient.address
  }),
  getMintToCheckedInstruction({
    mint: mint.address,
    token: sourceToken,
    mintAuthority: client.payer,
    amount: 1n,
    decimals: 0
  })
]);

try {
  await client.sendTransaction([
    // !mark(1:8)
    getTransferCheckedInstruction({
      source: sourceToken, // Token account sending the transfer.
      mint: mint.address, // Mint with the non-transferable configuration.
      destination: destinationToken, // Token account receiving the transfer.
      authority: client.payer, // Signer approving the transfer.
      amount: 1n, // Token amount in base units.
      decimals: 0 // Decimals defined on the mint.
    })
  ]);
} catch (error) {
  console.error("Transfer failed as expected:", error);
}

const mintAccount = await fetchMint(client.rpc, mint.address);
const sourceTokenAccount = await fetchToken(client.rpc, sourceToken);

console.log("Mint Address:", mint.address);
console.log("Mint Extensions:", mintAccount.data.extensions);
console.log("\nSource ATA:", sourceToken);
console.log("Source Token Extensions:", sourceTokenAccount.data.extensions);
console.log("Destination ATA:", destinationToken);
```

</CodeTabs>

#### Web3.js

<CodeTabs storage="token-ts-legacy" flags="r">

```ts !! title="Instructions"
import {
  Connection,
  Keypair,
  LAMPORTS_PER_SOL,
  sendAndConfirmTransaction,
  SystemProgram,
  Transaction
} from "@solana/web3.js";
import {
  ASSOCIATED_TOKEN_PROGRAM_ID,
  createAssociatedTokenAccountInstruction,
  createInitializeMintInstruction,
  createInitializeNonTransferableMintInstruction,
  createMintToCheckedInstruction,
  createTransferCheckedInstruction,
  ExtensionType,
  getAccount,
  getAssociatedTokenAddressSync,
  getMint,
  getMintLen,
  getNonTransferable,
  TOKEN_2022_PROGRAM_ID
} from "@solana/spl-token";

const connection = new Connection("http://localhost:8899", "confirmed");
const latestBlockhash = await connection.getLatestBlockhash();

const feePayer = Keypair.generate();
const recipient = Keypair.generate();

const airdropSignature = await connection.requestAirdrop(
  feePayer.publicKey,
  LAMPORTS_PER_SOL
);
await connection.confirmTransaction({
  blockhash: latestBlockhash.blockhash,
  lastValidBlockHeight: latestBlockhash.lastValidBlockHeight,
  signature: airdropSignature
});

const mint = Keypair.generate();
const mintSpace = getMintLen([ExtensionType.NonTransferable]);
const mintRent = await connection.getMinimumBalanceForRentExemption(mintSpace);

const sourceToken = getAssociatedTokenAddressSync(
  mint.publicKey,
  feePayer.publicKey,
  false,
  TOKEN_2022_PROGRAM_ID,
  ASSOCIATED_TOKEN_PROGRAM_ID
);

const destinationToken = getAssociatedTokenAddressSync(
  mint.publicKey,
  recipient.publicKey,
  false,
  TOKEN_2022_PROGRAM_ID,
  ASSOCIATED_TOKEN_PROGRAM_ID
);

await sendAndConfirmTransaction(
  connection,
  new Transaction({
    feePayer: feePayer.publicKey,
    blockhash: latestBlockhash.blockhash,
    lastValidBlockHeight: latestBlockhash.lastValidBlockHeight
  }).add(
    SystemProgram.createAccount({
      fromPubkey: feePayer.publicKey,
      newAccountPubkey: mint.publicKey,
      lamports: mintRent,
      space: mintSpace,
      programId: TOKEN_2022_PROGRAM_ID
    }),
    // !mark(1:4)
    createInitializeNonTransferableMintInstruction(
      mint.publicKey, // Mint account that stores the NonTransferable extension.
      TOKEN_2022_PROGRAM_ID // Token program that owns the mint.
    ),
    createInitializeMintInstruction(
      mint.publicKey,
      0,
      feePayer.publicKey,
      feePayer.publicKey,
      TOKEN_2022_PROGRAM_ID
    )
  ),
  [feePayer, mint],
  { commitment: "confirmed" }
);

await sendAndConfirmTransaction(
  connection,
  new Transaction().add(
    createAssociatedTokenAccountInstruction(
      feePayer.publicKey,
      sourceToken,
      feePayer.publicKey,
      mint.publicKey,
      TOKEN_2022_PROGRAM_ID,
      ASSOCIATED_TOKEN_PROGRAM_ID
    ),
    createAssociatedTokenAccountInstruction(
      feePayer.publicKey,
      destinationToken,
      recipient.publicKey,
      mint.publicKey,
      TOKEN_2022_PROGRAM_ID,
      ASSOCIATED_TOKEN_PROGRAM_ID
    ),
    createMintToCheckedInstruction(
      mint.publicKey,
      sourceToken,
      feePayer.publicKey,
      1,
      0,
      [],
      TOKEN_2022_PROGRAM_ID
    )
  ),
  [feePayer],
  { commitment: "confirmed" }
);

try {
  await sendAndConfirmTransaction(
    connection,
    new Transaction().add(
      // !mark(1:10)
      createTransferCheckedInstruction(
        sourceToken, // Token account sending the transfer.
        mint.publicKey, // Mint with the non-transferable configuration.
        destinationToken, // Token account receiving the transfer.
        feePayer.publicKey, // Signer approving the transfer.
        1, // Token amount in base units.
        0, // Decimals defined on the mint.
        [], // Additional multisig signers.
        TOKEN_2022_PROGRAM_ID // Token program that processes the transfer.
      )
    ),
    [feePayer],
    { commitment: "confirmed" }
  );
} catch (error) {
  console.error("Transfer failed as expected:", error);
}

const mintAccount = await getMint(
  connection,
  mint.publicKey,
  "confirmed",
  TOKEN_2022_PROGRAM_ID
);
const sourceTokenAccount = await getAccount(
  connection,
  sourceToken,
  "confirmed",
  TOKEN_2022_PROGRAM_ID
);

console.log("Mint Address:", mint.publicKey.toBase58());
console.log("Has NonTransferable:", getNonTransferable(mintAccount) !== null);
console.log("\nSource ATA:", sourceToken.toBase58());
console.log("Source Token Account:", sourceTokenAccount);
console.log("Destination ATA:", destinationToken.toBase58());
```

</CodeTabs>

### Rust

<CodeTabs storage="token-rs" flags="r">

```rust !! title="Rust"
use anyhow::Result;
use solana_client::nonblocking::rpc_client::RpcClient;
use solana_commitment_config::CommitmentConfig;
use solana_sdk::{
    signature::{Keypair, Signer},
    transaction::Transaction,
};
use solana_system_interface::instruction::create_account;
use spl_associated_token_account_interface::{
    address::get_associated_token_address_with_program_id,
    instruction::create_associated_token_account,
};
use spl_token_2022_interface::{
    extension::{
        non_transferable::{NonTransferable, NonTransferableAccount},
        BaseStateWithExtensions, ExtensionType, StateWithExtensions,
    },
    instruction::{
        initialize_mint, initialize_non_transferable_mint, mint_to_checked, transfer_checked,
    },
    state::{Account, Mint},
    ID as TOKEN_2022_PROGRAM_ID,
};

#[tokio::main]
async fn main() -> Result<()> {
    let client = RpcClient::new_with_commitment(
        String::from("http://localhost:8899"),
        CommitmentConfig::confirmed(),
    );
    let recipient = Keypair::new();
    let fee_payer = Keypair::new();
    let decimals = 0;

    let airdrop_signature = client
        .request_airdrop(&fee_payer.pubkey(), 1_000_000_000)
        .await?;
    loop {
        let confirmed = client.confirm_transaction(&airdrop_signature).await?;
        if confirmed {
            break;
        }
    }

    let mint = Keypair::new();
    let mint_space =
        ExtensionType::try_calculate_account_len::<Mint>(&[ExtensionType::NonTransferable])?;
    let mint_rent = client
        .get_minimum_balance_for_rent_exemption(mint_space)
        .await?;

    let mint_blockhash = client.get_latest_blockhash().await?;
    let mint_transaction = Transaction::new_signed_with_payer(
        &[
            create_account(
                &fee_payer.pubkey(),
                &mint.pubkey(),
                mint_rent,
                mint_space as u64,
                &TOKEN_2022_PROGRAM_ID,
            ),
            initialize_non_transferable_mint(&TOKEN_2022_PROGRAM_ID, &mint.pubkey())?,
            initialize_mint(
                &TOKEN_2022_PROGRAM_ID,
                &mint.pubkey(),
                &fee_payer.pubkey(),
                Some(&fee_payer.pubkey()),
                decimals,
            )?,
        ],
        Some(&fee_payer.pubkey()),
        &[&fee_payer, &mint],
        mint_blockhash,
    );
    client.send_and_confirm_transaction(&mint_transaction).await?;

    let source_token_address = get_associated_token_address_with_program_id(
        &fee_payer.pubkey(),
        &mint.pubkey(),
        &TOKEN_2022_PROGRAM_ID,
    );
    let destination_token_address = get_associated_token_address_with_program_id(
        &recipient.pubkey(),
        &mint.pubkey(),
        &TOKEN_2022_PROGRAM_ID,
    );

    let setup_blockhash = client.get_latest_blockhash().await?;
    let setup_transaction = Transaction::new_signed_with_payer(
        &[
            create_associated_token_account(
                &fee_payer.pubkey(),
                &fee_payer.pubkey(),
                &mint.pubkey(),
                &TOKEN_2022_PROGRAM_ID,
            ),
            create_associated_token_account(
                &fee_payer.pubkey(),
                &recipient.pubkey(),
                &mint.pubkey(),
                &TOKEN_2022_PROGRAM_ID,
            ),
            mint_to_checked(
                &TOKEN_2022_PROGRAM_ID,
                &mint.pubkey(),
                &source_token_address,
                &fee_payer.pubkey(),
                &[],
                1,
                decimals,
            )?,
        ],
        Some(&fee_payer.pubkey()),
        &[&fee_payer],
        setup_blockhash,
    );
    client.send_and_confirm_transaction(&setup_transaction).await?;

    let transfer_blockhash = client.get_latest_blockhash().await?;
    let transfer_transaction = Transaction::new_signed_with_payer(
        &[
            // !mark(1:10)
            transfer_checked(
                &TOKEN_2022_PROGRAM_ID, // Token program to invoke.
                &source_token_address, // Token account sending the tokens.
                &mint.pubkey(), // Mint for the token being transferred.
                &destination_token_address, // Token account receiving the tokens.
                &fee_payer.pubkey(), // Owner or delegate approving the transfer.
                &[], // Additional multisig signers.
                1, // Token amount in base units.
                decimals, // Decimals defined on the mint account.
            )?,
        ],
        Some(&fee_payer.pubkey()),
        &[&fee_payer],
        transfer_blockhash,
    );

    match client.send_and_confirm_transaction(&transfer_transaction).await {
        Ok(signature) => println!("Transfer unexpectedly succeeded: {}", signature),
        Err(error) => println!("Transfer failed as expected: {error:#?}"),
    }

    let mint_account = client.get_account(&mint.pubkey()).await?;
    let mint_state = StateWithExtensions::<Mint>::unpack(&mint_account.data)?;
    let source_token_account = client.get_account(&source_token_address).await?;
    let source_token_state = StateWithExtensions::<Account>::unpack(&source_token_account.data)?;

    println!("Mint Address: {}", mint.pubkey());
    println!("Mint Extensions: {:?}", mint_state.get_extension_types()?);
    println!(
        "Has NonTransferable: {}",
        mint_state.get_extension::<NonTransferable>().is_ok()
    );
    println!("\nSource ATA: {}", source_token_address);
    println!(
        "Source Token Extensions: {:?}",
        source_token_state.get_extension_types()?
    );
    println!(
        "Has NonTransferableAccount: {}",
        source_token_state
            .get_extension::<NonTransferableAccount>()
            .is_ok()
    );
    println!("Destination ATA: {}", destination_token_address);

    Ok(())
}
```

</CodeTabs>
