---
title: Create a Token Account
description:
  Learn how to create a token account with the Confidential Transfer extension.
---

## How to create a token account with the Confidential Transfer extension

The Confidential Transfer extension enables private token transfers by adding
extra state to the token account. This section explains how to create a token
account with this extension enabled.

The following diagram shows the steps involved in creating a token account with
the Confidential Transfer extension:

```mermaid title="Create Token Account with Confidential Transfer Extension"
sequenceDiagram
    participant Payer as Wallet
    participant ATA as Associated Token Program
    participant Token22 as Token Extensions Program
    participant ZK as ZK ElGamal Proof Program
    participant TokenAccount as Token Account

    Payer->>ATA: create_associated_token_account()
    activate ATA
    ATA->>Token22: Create token account at deterministic address
    deactivate ATA
    activate Token22
    Token22-->>TokenAccount: Initialize Token Account
    deactivate Token22

    Payer->>Token22: reallocate() for ConfidentialTransferAccount extension
    activate Token22
    Token22-->>TokenAccount: Add space for extension data
    deactivate Token22

    Payer->>Payer: Derive ElGamal keypair and AES key
    Payer->>Payer: Generate pubkey validity proof
    Payer->>Payer: Encrypt initial balance (0) with AES key

    Payer->>ZK: VerifyPubkeyValidity into context state account
    activate ZK
    ZK-->>ZK: Verify proof and store result in context state account
    deactivate ZK

    Payer->>Token22: configure_account() referencing the proof context account
    activate Token22
    Token22-->>TokenAccount: Configure for confidential transfers
    deactivate Token22
```

### Confidential Transfer Token Account State

The extension adds the
[ConfidentialTransferAccount](https://github.com/solana-program/token-2022/blob/efd0c957fefbd79882d77df5fb2dac88c001249c/program/src/extension/confidential_transfer/mod.rs#L75-L122)
state to the token account:

```rust title="Confidential Token Account State"
#[repr(C)]
#[derive(Clone, Copy, Debug, Default, PartialEq, Pod, Zeroable)]
pub struct ConfidentialTransferAccount {
    /// `true` if this account has been approved for use. All confidential
    /// transfer operations for the account will fail until approval is
    /// granted.
    pub approved: PodBool,

    /// The public key associated with ElGamal encryption
    pub elgamal_pubkey: PodElGamalPubkey,

    /// The low 16 bits of the pending balance (encrypted by `elgamal_pubkey`)
    pub pending_balance_lo: EncryptedBalance,

    /// The high 48 bits of the pending balance (encrypted by `elgamal_pubkey`)
    pub pending_balance_hi: EncryptedBalance,

    /// The available balance (encrypted by `encryption_pubkey`)
    pub available_balance: EncryptedBalance,

    /// The decryptable available balance
    pub decryptable_available_balance: DecryptableBalance,

    /// If `false`, the extended account rejects any incoming confidential
    /// transfers
    pub allow_confidential_credits: PodBool,

    /// If `false`, the base account rejects any incoming transfers
    pub allow_non_confidential_credits: PodBool,

    /// The total number of `Deposit` and `Transfer` instructions that have
    /// credited `pending_balance`
    pub pending_balance_credit_counter: PodU64,

    /// The maximum number of `Deposit` and `Transfer` instructions that can
    /// credit `pending_balance` before the `ApplyPendingBalance`
    /// instruction is executed
    pub maximum_pending_balance_credit_counter: PodU64,

    /// The `expected_pending_balance_credit_counter` value that was included in
    /// the last `ApplyPendingBalance` instruction
    pub expected_pending_balance_credit_counter: PodU64,

    /// The actual `pending_balance_credit_counter` when the last
    /// `ApplyPendingBalance` instruction was executed
    pub actual_pending_balance_credit_counter: PodU64,
}
```

The _rs`ConfidentialTransferAccount`_ contains several fields to manage
confidential transfers:

- **approved**: The account's approval status for confidential transfers. If the
  mint account's `auto_approve_new_accounts` configuration is set as `true`, all
  token accounts are automatically approved for confidential transfers.

- **elgamal_pubkey**: The ElGamal public key used to encrypt balances and
  transfer amounts.

- **pending_balance_lo**: The encrypted lower 16 bits of the pending balance.
  The balance is split into high and low parts for efficient decryption.

- **pending_balance_hi**: The encrypted higher 48 bits of the pending balance.
  The balance is split into high and low parts for efficient decryption.

- **available_balance**: The encrypted balance available for transfers.

- **decryptable_available_balance**: The available balance encrypted with an
  Advanced Encryption Standard (AES) key for efficient decryption by the account
  owner.

- **allow_confidential_credits**: If true, allows incoming confidential
  transfers.

- **allow_non_confidential_credits**: If true, allows incoming non-confidential
  transfers.

- **pending_balance_credit_counter**: Counts incoming pending balance credits
  from deposit and transfer instructions.

- **maximum_pending_balance_credit_counter**: The count limit of pending credits
  before requiring an _rs`ApplyPendingBalance`_ instruction to convert the
  pending balance to the available balance.

- **expected_pending_balance_credit_counter**: The
  `pending_balance_credit_counter` value provided by the client through the
  instruction data the last time the _rs`ApplyPendingBalance`_ instruction was
  processed.

- **actual_pending_balance_credit_counter**: The
  `pending_balance_credit_counter` value on the token account at the time the
  last _rs`ApplyPendingBalance`_ instruction was processed.

<Callout type="info">
  **Pending vs Available Balance**

Confidential balances are separated into pending and available balances to
prevent DoS attacks. Without this separation, an attacker could repeatedly send
tokens to a token account, blocking the token account owner's ability to
transfer tokens. The token account owner would be unable to transfer tokens
because the encrypted balance would change between when the transaction is
submitted and when it is processed, resulting in a failed transaction.

All deposits and transfer amounts are initially added to the pending balance.
Token account owners must use the _rs`ApplyPendingBalance`_ instruction to
convert the pending balance to the available balance. Incoming transfers or
deposits don't affect a token account's available balance.

</Callout>

<Callout type="info">
  **Pending Balance High/Low Split**

The confidential pending balance is split into `pending_balance_lo` and
`pending_balance_hi` because ElGamal decryption requires more computation for
larger numbers. You can find the ciphertext arithmetic implementation
[here](https://github.com/solana-program/token-2022/blob/efd0c957fefbd79882d77df5fb2dac88c001249c/confidential-transfer/ciphertext-arithmetic/src/lib.rs#L54),
which is used in the _rs`ApplyPendingBalance`_ instruction
[here](https://github.com/solana-program/token-2022/blob/efd0c957fefbd79882d77df5fb2dac88c001249c/program/src/extension/confidential_transfer/processor.rs#L1180-L1184).

</Callout>

<Callout type="info">
  **Pending Balance Credit Counters**

When calling the _rs`ApplyPendingBalance`_ instruction to convert the pending
balance to the available balance:

1. The client looks up current pending and available balances, encrypts the sum,
   and provides a `decryptable_available_balance` encrypted using the token
   account owner's AES key.

2. The expected and actual pending credit counters track changes to the counter
   value between when the _rs`ApplyPendingBalance`_ instruction is created and
   processed:
   - `expected_pending_balance_credit_counter`: The
     `pending_balance_credit_counter` value when the client creates the
     _rs`ApplyPendingBalance`_ instruction
   - `actual_pending_balance_credit_counter`: The
     `pending_balance_credit_counter` value on the token account at the time the
     _rs`ApplyPendingBalance`_ instruction is processed

Matching expected/actual counters indicate the `decryptable_available_balance`
matches the `available_balance`.

When fetching a token account's state to read the
`decryptable_available_balance`, different expected/actual counters values
require the client to look up recent deposit/transfer instructions matching the
counter difference to calculate the correct balance.

**Balance Reconciliation Process**

When the expected and actual pending balance counters differ, follow these steps
to reconcile the `decryptable_available_balance`:

1. Start with the `decryptable_available_balance` from the token account
2. Fetch the most recent transactions including deposit and transfer
   instructions up to the counter difference (actual - expected):
   - Add public amounts from deposit instructions
   - Decrypt and add destination ciphertext amounts from transfer instructions

</Callout>

### Required Instructions

Creating and configuring a token account for confidential transfers uses the
following instructions, which all fit in a single transaction:

1. **Create the Token Account**: Invoke the Associated Token Program's
   _rs`AssociatedTokenAccountInstruction::Create`_ instruction to create the
   token account at its deterministic address.

2. **Reallocate Account Space**: Invoke the Token Extension Program's
   _rs`TokenInstruction::Reallocate`_ instruction to add space for the
   _rs`ConfidentialTransferAccount`_ state.

3. **Verify the Pubkey Validity Proof**: Create an account owned by the ZK
   ElGamal Proof program, then invoke its _rs`VerifyPubkeyValidity`_ instruction
   to verify the proof and store the verified result in that context state
   account.

4. **Configure Confidential Transfers**: Invoke the Token Extension Program's
   [ConfidentialTransferInstruction::ConfigureAccount](https://github.com/solana-program/token-2022/blob/efd0c957fefbd79882d77df5fb2dac88c001249c/program/src/extension/confidential_transfer/processor.rs#L201)
   instruction, referencing the proof context state account via
   _rs`ProofLocation::ContextStateAccount`_, to initialize the
   _rs`ConfidentialTransferAccount`_ state.

<Callout type="info">

**Only the token account owner can configure a token account for confidential
transfers**.

The _rs`ConfigureAccount`_ instruction requires client-side generation of
encryption keys and a proof that can only be generated by the token account
owner.

The pubkey validity proof verifies that the account's ElGamal public key is
valid. It is generated with _rs`build_pubkey_validity_proof_data`_, verified on
chain by the ZK ElGamal Proof program into a context state account, and then
referenced from _rs`ConfigureAccount`_ via
_rs`ProofLocation::ContextStateAccount`_, so no proof bytes travel in the token
instruction itself. For implementation details, see:

- [PubkeyValidityProofData](https://github.com/anza-xyz/agave/blob/b39a9764005ac8596b7e14e61d381305ce57ecaa/zk-sdk/src/zk_elgamal_proof_program/proof_data/pubkey_validity.rs#L58)
- [PubkeyValidityProof](https://github.com/anza-xyz/agave/blob/b39a9764005ac8596b7e14e61d381305ce57ecaa/zk-sdk/src/sigma_proofs/pubkey_validity.rs#L65)

</Callout>

## Example Code

The following code creates an associated token account and configures it for
confidential transfers against an existing confidential mint.

Confidential transfers depend on the ZK ElGamal Proof program, which is enabled
on mainnet and devnet. A stock `solana-test-validator` does not enable it, but a
mainnet-forking local validator such as [Surfpool](https://surfpool.run) does.
Run the example against one of those (the code uses devnet) with a funded payer,
and replace the mint placeholder with a mint created per
[Create a Mint](/docs/tokens/extensions/confidential-transfer/create-mint).

### Rust

<CodeTabs>

```rust !! title="main.rs"
// !collapse(1:36) collapsed
// Imports: dependencies used by this example.
use anyhow::{Context, Result};
use solana_address::Address;
use solana_client::rpc_client::RpcClient;
use solana_commitment_config::CommitmentConfig;
use solana_keypair::Keypair;
use solana_pubkey::Pubkey;
use solana_signer::Signer;
use solana_system_interface::instruction as system_instruction;
use solana_transaction::Transaction;
use solana_zk_elgamal_proof_interface::{
    instruction::{ContextStateInfo, ProofInstruction},
    proof_data::PubkeyValidityProofContext,
    state::ProofContextState,
};
use solana_zk_sdk::{
    encryption::{derivation::derive_confidential_keys, elgamal::ElGamalKeypair},
    zk_elgamal_proof_program::pubkey_validity::build_pubkey_validity_proof_data,
};
use solana_zk_sdk_pod::encryption::{auth_encryption::PodAeCiphertext, elgamal::PodElGamalPubkey};
use spl_associated_token_account::{
    get_associated_token_address_with_program_id, instruction::create_associated_token_account,
};
use spl_token_2022::{
    extension::{
        confidential_transfer::instruction::{
            configure_account, initialize_mint as initialize_confidential_transfer_mint,
            PubkeyValidityProofData,
        },
        ExtensionType,
    },
    instruction::{initialize_mint as initialize_mint_base, reallocate},
    state::Mint,
};
use spl_token_confidential_transfer_proof_extraction::instruction::ProofLocation;
use std::mem::size_of;

// The native ZK ElGamal Proof program verifies the proof on chain.
const ZK_PROOF_PROGRAM_ID: Pubkey =
    solana_pubkey::pubkey!("ZkE1Gama1Proof11111111111111111111111111111");

fn main() -> Result<()> {
    // Use a cluster whose ZK ElGamal Proof program is enabled (mainnet, devnet).
    let rpc_client = RpcClient::new_with_commitment(
        String::from("https://api.devnet.solana.com"),
        CommitmentConfig::confirmed(),
    );

    // The Solana CLI default keypair, used as fee payer, mint authority, and
    // token account owner.
    let payer = load_keypair()?;
    let decimals: u8 = 2;

    // Setup: create a confidential mint for the token account.
    let mint = create_confidential_mint(&rpc_client, &payer, decimals)?;

    let token_account = get_associated_token_address_with_program_id(
        &payer.pubkey(),
        &mint,
        &spl_token_2022::id(),
    );

    // 1. Create the associated token account.
    let create_ata_ix = create_associated_token_account(
        &payer.pubkey(), // funding account
        &payer.pubkey(), // token account owner
        &mint,
        &spl_token_2022::id(),
    );

    // 2. Add space for the ConfidentialTransferAccount extension.
    let realloc_ix = reallocate(
        &spl_token_2022::id(),
        &token_account,
        &payer.pubkey(), // payer
        &payer.pubkey(), // owner
        &[&payer.pubkey()],
        &[ExtensionType::ConfidentialTransferAccount],
    )?;

    // 3. Derive the owner's ElGamal keypair and AES key from a signature over
    //    the token account address. The same signer and address always derive
    //    the same keys, so the owner can recover them from their wallet.
    let (elgamal_keypair, aes_key) = derive_confidential_keys(&payer, &token_account.to_bytes())
        .map_err(|e| anyhow::anyhow!("derive confidential keys: {e}"))?;

    // Initial decryptable available balance of 0, encrypted with the AES key.
    let decryptable_balance: PodAeCiphertext = aes_key.encrypt(0).into();
    let maximum_pending_balance_credit_counter: u64 = 65_536;

    // 4. Generate the pubkey-validity proof, then pre-verify it into a context
    //    state account owned by the ZK ElGamal Proof program. configure_account
    //    references the verified proof by account, so no proof bytes travel in
    //    the token instruction itself.
    let proof_data = build_pubkey_validity_proof_data(&elgamal_keypair)
        .map_err(|e| anyhow::anyhow!("generate pubkey validity proof: {e}"))?;

    let proof_account = Keypair::new();
    let context_state_size = size_of::<ProofContextState<PubkeyValidityProofContext>>();
    let context_state_rent =
        rpc_client.get_minimum_balance_for_rent_exemption(context_state_size)?;

    let create_proof_account_ix = system_instruction::create_account(
        &payer.pubkey(),
        &proof_account.pubkey(),
        context_state_rent,
        context_state_size as u64,
        &ZK_PROOF_PROGRAM_ID,
    );

    let proof_account_address: Address = proof_account.pubkey().to_bytes().into();
    let owner_address: Address = payer.pubkey().to_bytes().into();
    let verify_proof_ix = ProofInstruction::VerifyPubkeyValidity.encode_verify_proof(
        Some(ContextStateInfo {
            context_state_account: &proof_account_address,
            context_state_authority: &owner_address,
        }),
        &proof_data,
    );

    // 5. Configure the account, pointing at the pre-verified proof account.
    let proof_location: ProofLocation<PubkeyValidityProofData> =
        ProofLocation::ContextStateAccount(&proof_account.pubkey());
    let configure_account_ixs = configure_account(
        &spl_token_2022::id(),
        &token_account,
        &mint,
        &decryptable_balance,
        maximum_pending_balance_credit_counter,
        &payer.pubkey(), // owner
        &[],
        proof_location,
    )?;

    // Everything fits in a single transaction.
    let mut instructions = vec![
        create_ata_ix,
        realloc_ix,
        create_proof_account_ix,
        verify_proof_ix,
    ];
    instructions.extend(configure_account_ixs);

    let blockhash = rpc_client.get_latest_blockhash()?;
    let transaction = Transaction::new_signed_with_payer(
        &instructions,
        Some(&payer.pubkey()),
        &[&payer, &proof_account],
        blockhash,
    );

    let signature = rpc_client.send_and_confirm_transaction(&transaction)?;
    println!("Configured token account {token_account} for confidential transfers: {signature}");

    Ok(())
}

// !collapse(1:1000) collapsed
// Setup: helper function to create the confidential mint.
fn create_confidential_mint(rpc_client: &RpcClient, payer: &Keypair, decimals: u8) -> Result<Pubkey> {
    let mint = Keypair::new();

    // Allocate space for a mint that carries the ConfidentialTransferMint
    // extension, then fund it for rent exemption.
    let space = ExtensionType::try_calculate_account_len::<Mint>(&[
        ExtensionType::ConfidentialTransferMint,
    ])?;
    let rent = rpc_client.get_minimum_balance_for_rent_exemption(space)?;

    // The auditor ElGamal key lets the issuer decrypt transfer amounts for
    // compliance. Persist this key. Pass `None` to create a mint with no auditor.
    let auditor = ElGamalKeypair::new_rand();
    let auditor_pubkey: PodElGamalPubkey = (*auditor.pubkey()).into();

    let create_account_ix = system_instruction::create_account(
        &payer.pubkey(),
        &mint.pubkey(),
        rent,
        space as u64,
        &spl_token_2022::id(),
    );

    // The confidential-transfer extension must be initialized before the base
    // mint and cannot be added later.
    let init_confidential_ix = initialize_confidential_transfer_mint(
        &spl_token_2022::id(),
        &mint.pubkey(),
        Some(payer.pubkey()), // authority that can update confidential settings
        true,                 // auto-approve new accounts
        Some(auditor_pubkey),
    )?;

    let init_mint_ix = initialize_mint_base(
        &spl_token_2022::id(),
        &mint.pubkey(),
        &payer.pubkey(), // mint authority
        None,            // freeze authority
        decimals,
    )?;

    let blockhash = rpc_client.get_latest_blockhash()?;
    let transaction = Transaction::new_signed_with_payer(
        &[create_account_ix, init_confidential_ix, init_mint_ix],
        Some(&payer.pubkey()),
        &[payer, &mint],
        blockhash,
    );
    let signature = rpc_client.send_and_confirm_transaction(&transaction)?;
    println!("Created confidential mint {}: {signature}", mint.pubkey());

    Ok(mint.pubkey())
}

// Load the Solana CLI default keypair (~/.config/solana/id.json).
fn load_keypair() -> Result<Keypair> {
    let keypair_path = dirs::home_dir()
        .context("could not find home directory")?
        .join(".config/solana/id.json");
    let bytes: Vec<u8> = serde_json::from_reader(std::fs::File::open(keypair_path)?)?;
    let mut secret = [0u8; 32];
    secret.copy_from_slice(&bytes[0..32]);
    Ok(Keypair::new_from_array(secret))
}
```

```toml !! title="Cargo.toml"
[package]
name = "confidential-transfer"
version = "0.1.0"
edition = "2021"

# spl-token-2022 11 requires solana-system-interface 3.2 (which needs
# solana-instruction >= 3.4). The stable solana-client 4.0.0 caps it lower, so
# pin the 4.0.0-rc.0 line and use the granular solana crates instead of the
# solana-sdk umbrella. This collapses back to solana-sdk once a stable
# solana-client that allows solana-instruction 3.4 ships.
[dependencies]
solana-client = "4.0.0-rc.0"
solana-pubkey = "4.2"
solana-keypair = "3.1"
solana-signer = "3.0"
solana-transaction = "3.1"
solana-commitment-config = "3.1.1"
solana-system-interface = { version = "3.2.0", features = ["bincode"] }
solana-address = "2.6"
solana-zk-sdk = "7.0.1"
solana-zk-sdk-pod = "0.1.2"
solana-zk-elgamal-proof-interface = "0.1.2"
spl-token-2022 = { version = "11.0.0", features = ["zk-ops"] }
spl-associated-token-account = "8.0.0"
spl-token-confidential-transfer-proof-extraction = "0.6.1"

anyhow = "1.0"
dirs = "6.0.0"
serde_json = "1.0"
```

</CodeTabs>

### Typescript

<CodeTabs>

```ts !! title="index.ts"
// !collapse(1:21) collapsed
// Imports: dependencies used by this example.
import {
  deriveAeKeyForOwnerMint,
  deriveElGamalKeypairForOwnerMint,
  getCreateConfidentialTransferAccountInstructionPlan
} from "@solana-program/token-2022/confidential";
import {
  TOKEN_2022_PROGRAM_ADDRESS,
  findAssociatedTokenPda,
  getCreateMintInstructionPlan
} from "@solana-program/token-2022";
import { createClient, generateKeyPairSigner, some } from "@solana/kit";
import { solanaRpc } from "@solana/kit-plugin-rpc";
import { signerFromFile } from "@solana/kit-plugin-signer";
import {
  AeKey,
  ElGamalKeypair,
  ElGamalSecretKey
} from "@solana/zk-sdk/bundler";
import { homedir } from "node:os";
import { join } from "node:path";

const client = await createClient()
  .use(signerFromFile(join(homedir(), ".config/solana/id.json")))
  .use(
    solanaRpc({
      rpcUrl: "https://api.devnet.solana.com"
    })
  );

// The Solana CLI default keypair, used as fee payer, mint authority, and
// token account owner.
const owner = client.payer;
const decimals = 2;

// Setup: create a confidential mint for the token account.
const mint = await createConfidentialMint(client, owner, decimals);

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

// Derive recoverable ElGamal and AES keys bound to (owner, mint). Re-deriving
// from the same wallet always yields the same keys, so the owner can recover
// them rather than having to back up a separate secret.
const derivedElGamal = await deriveElGamalKeypairForOwnerMint({
  signer: owner,
  owner: owner.address,
  mint
});
const elgamalKeypair = ElGamalKeypair.fromSecretKey(
  ElGamalSecretKey.fromBytes(derivedElGamal.secretKey)
);
const aesKey = AeKey.fromBytes(
  await deriveAeKeyForOwnerMint({ signer: owner, owner: owner.address, mint })
);

// Build the create-ATA + reallocate + verify-proof + configure plan, then send.
// The helper returns an instruction plan because the steps may span more than
// one transaction.
const plan = await getCreateConfidentialTransferAccountInstructionPlan({
  rpc: client.rpc,
  payer: owner,
  owner,
  mint,
  elgamalKeypair,
  aesKey
});

const result = await client.sendTransaction(plan);
console.log(
  `Configured token account ${tokenAccount} for confidential transfers: ${result.context.signature}`
);

// !collapse(1:1000) collapsed
// Setup: helper function to create the confidential mint.
async function createConfidentialMint(
  kitClient: typeof client,
  payer: typeof owner,
  decimals: number
) {
  const mint = await generateKeyPairSigner();
  const auditor = await deriveElGamalKeypairForOwnerMint({
    signer: payer,
    owner: payer.address,
    mint: mint.address
  });

  const plan = getCreateMintInstructionPlan({
    payer,
    newMint: mint,
    decimals,
    mintAuthority: payer,
    extensions: [
      {
        __kind: "ConfidentialTransferMint",
        authority: some(payer.address),
        autoApproveNewAccounts: true,
        auditorElgamalPubkey: some(auditor.elgamalPubkey)
      }
    ]
  });

  await kitClient.sendTransaction(plan);

  return mint.address;
}
```

```json !! title="package.json"
{
  "name": "confidential-account",
  "version": "0.1.0",
  "type": "module",
  "dependencies": {
    "@solana-program/system": "^0.12.2",
    "@solana-program/token-2022": "^0.12.0",
    "@solana/kit": "^6.10.0",
    "@solana/kit-plugin-rpc": "^0.11.1",
    "@solana/kit-plugin-signer": "^0.10.0",
    "@solana/zk-sdk": "^0.4.2"
  },
  "devDependencies": {
    "@types/node": "^24.10.0",
    "typescript": "^5.8.3"
  }
}
```

</CodeTabs>

<Callout type="info">
  The TypeScript helpers live in the `@solana-program/token-2022/confidential`
  subpath and build on `@solana/zk-sdk` for the encryption primitives. `owner`
  and `client` come from your `@solana/kit` setup; the returned instruction plan
  is sent with `@solana/kit`'s instruction-plan support, which splits the work
  across transactions when the proofs are too large for one.
</Callout>
