Confidential Transfer Integration Guide

Supporting Confidential Transfers on Solana

Background

The Confidential Transfer extension lets Token-2022 mints keep transfer amounts and account balances encrypted onchain. Balances are encrypted, so displaying them requires the owner's decryption keys, and sending requires generating zero-knowledge proofs on the client.

This guide is for teams integrating confidential transfer tokens (wallets, explorers, exchanges, custodians, indexers) rather than issuers configuring a mint. If you are building the underlying flow from scratch, start with the step-by-step pages linked below; this guide focuses on what your product needs to do to support these tokens well.

Account addresses, the mint, and the owner of each account stay public. Only amounts and balances are encrypted, so everything else, including who transacts with whom, remains visible. This gives confidentiality from public observers, not anonymity.

Resources

TL;DR

  • Every confidential token account still has a public balance plus an encrypted pending and available balance. A token can move freely between public and confidential states.
  • To display a confidential balance you need the owner's keys. The recommended approach is to derive them from the owner's wallet, decrypt the available balance with the AES key (fast), and decrypt pending amounts with the ElGamal key when needed.
  • To send confidentially you generate ZK proofs on the client (equality, ciphertext validity, range), usually placed in temporary proof context state accounts, then submit the transfer. Both @solana-program/token-2022 (JS) and spl-token-client (Rust) provide high-level helpers that build the proofs and sequence the transactions for you.
  • Confidential transfers currently span a few dependent transactions because the proofs exceed today's transaction size limit. An upcoming change (transaction format v1, landing with Agave v4.2) raises that limit and is expected to let a confidential transfer execute in a single onchain transaction.
  • A wallet that adds no support still works: it shows the public balance and standard transfers keep functioning. Confidential funds stay accessible to any confidential-aware client with the owner's keys.

Terms

  • ElGamal keypair: per-account public-key keypair used to encrypt balances and build ZK proofs. The public key is stored on the account.
  • AES key: per-account symmetric key used to encrypt the "decryptable available balance" so the owner can read their available balance in constant time, without solving a discrete log.
  • Pending balance: encrypted balance of funds received via deposits or incoming transfers that have not yet been applied. Cannot be spent directly.
  • Available balance: encrypted balance that can be transferred or withdrawn.
  • Apply: the step that moves pending into available.
  • Proof context state account: a temporary onchain account holding a pre-verified ZK proof, referenced by a confidential instruction and then closed to reclaim rent.
  • Auditor: an optional global ElGamal key on the mint; when set, every transfer additionally encrypts its amount under this key so a designated party can decrypt it.

Account model and balances

When an account is configured for confidential transfers, the ConfidentialTransferAccount extension stores (among other fields):

  • elgamal_pubkey: the account's ElGamal public key.
  • pending_balance_lo / pending_balance_hi: ElGamal ciphertexts of the pending balance, split into low and high bits (decryption of the high bits is more expensive, so they are kept separate).
  • available_balance: ElGamal ciphertext of the spendable balance.
  • decryptable_available_balance: an AES ciphertext of the same available balance that the owner can decrypt instantly. This is the field to use for display.
  • allow_confidential_credits / allow_non_confidential_credits: whether the account currently accepts incoming confidential or public credits.
  • pending_balance_credit_counter and maximum_pending_balance_credit_counter: how many credits have accrued to the pending balance and the cap before an apply is required.

All of these fields are returned in parsed account state over standard RPC. Anyone can read the ciphertexts, but only key holders can recover the underlying amounts.

Key management

Each confidential account uses two keys: an ElGamal keypair for encryption and proofs, and an AES key for fast balance decryption. How you produce and store those keys is an integration choice. A few common options:

  • Derive from a wallet signature (recommended default). The owner signs a canonical, domain-separated message and the keys are derived from that signature, so they are reproducible from the wallet alone and you do not have to store them. The seed is deterministic per account: the JS helpers below bind it to the (owner, mint) pair, while the Rust example seeds from the token account address (for an associated token account these are equivalent, since that address is itself derived from the owner and mint).
  • Derive from independent key material. For passkeys, secure enclaves, or MPC setups, derive the keys from a WebAuthn PRF output or other input key material so that the confidential keys are not tied to the account's signing key. @solana/zk-sdk exposes ConfidentialKeys.fromIkm and ConfidentialKeys.fromPrf for this.
  • Generate and manage directly. Custodial and MPC providers may prefer to generate the keys and manage them like any other sensitive key material.

The @solana-program/token-2022 client ships helpers for the recommended path:

import {
deriveElGamalKeypairForOwnerMint,
deriveAeKeyForOwnerMint
} from "@solana-program/token-2022";
// `owner` signs a domain-separated message; the keys are bound to (owner, mint).
const { elgamalPubkey, secretKey } = await deriveElGamalKeypairForOwnerMint({
signer: owner,
owner: owner.address,
mint
});
const aesKey = await deriveAeKeyForOwnerMint({
signer: owner,
owner: owner.address,
mint
});

Confidential keys are decryption keys. Treat a request to sign the derivation message like unlocking a private view of the user's balances. Prefer deriving on demand over storing them; if you do store them (for example in a custodial service), protect them with the same rigor as signing keys.

The derivation helpers return serialized key material. The high-level operation helpers (shown later) take WASM ElGamalKeypair and AeKey objects, so rebuild them from the bytes when you need them:

Creating and configuring accounts

Before an account can hold a confidential balance it needs the ConfidentialTransferAccount extension, which adds roughly 295 bytes to the token account (on the order of 0.0015 SOL in extra rent). Setup is two steps: create the token account, then configure the extension.

Configuring normally requires the account owner to sign and supply a proof that they own the ElGamal public key being set on the account. A wallet or exchange can create and fund the bare token account for a user, but it cannot configure the confidential extension on the user's behalf without that signature.

Because of this, avoid silently provisioning confidential accounts for every user: it costs extra rent and still needs owner involvement. Configure on first use, or when the user opts into confidential transfers.

The ElGamal registry removes the per-account owner step. A user registers their ElGamal public key once (signing a proof at registration), and the registry entry is reusable across every mint. After that, a third party can configure confidential accounts for the user with ConfigureAccountWithRegistry, which needs no owner signature or proof at configuration time, only a payer for rent. This is the mechanism to use if you want to provision confidential accounts for users smoothly.

Registry configuration is available in the Rust spl-token-client (confidential_transfer_configure_token_account_with_registry) and the spl-elgamal-registry program today. Equivalent helpers in the @solana-program/token-2022 JS client are not available yet, so JS integrations that need the registry path should track that client's releases.

Displaying balances

A robust integration shows the public balance using the standard token balance, detects the confidential extension, and, when the user has unlocked their keys, decrypts and shows the available balance.

The available balance is best read from decryptable_available_balance using the AES key, which is constant time. Decrypting the ElGamal available_balance directly requires solving a discrete log and should be avoided for routine display.

import { AeCiphertext } from "@solana/zk-sdk/bundler";
import { fetchToken } from "@solana-program/token-2022";
import { unwrapOption } from "@solana/kit";
const account = await fetchToken(rpc, tokenAccountAddress);
// `extensions` is an Option<Array<Extension>>; each extension is a tagged union.
const extensions = unwrapOption(account.data.extensions) ?? [];
const ct = extensions.find((e) => e.__kind === "ConfidentialTransferAccount");
if (ct) {
// Fast path: decrypt the AES "decryptable available balance" for display.
const ciphertext = AeCiphertext.fromBytes(
new Uint8Array(ct.decryptableAvailableBalance)
);
// `aesKey` is the rebuilt AeKey object (see the rebuild snippet above), not raw bytes.
const availableBalance = ciphertext?.decrypt(aesKey); // bigint | undefined
console.log("Available (confidential):", availableBalance);
}

When the user has not unlocked their keys, show the public balance and indicate that a confidential balance exists but is locked, rather than showing zero.

Receiving transfers

Incoming deposits and transfers land in the pending balance and are not spendable until applied. Each credit increments pending_balance_credit_counter; once it reaches maximum_pending_balance_credit_counter, the account cannot receive more confidential credits until the owner applies the pending balance.

Integrations that hold funds on behalf of users should:

  • Surface pending vs available separately so users understand why a just-received amount is not yet spendable.
  • Apply the pending balance on the user's behalf at sensible times (for example before a send) so balances do not get stuck.
import { getApplyConfidentialPendingBalanceInstructionFromToken } from "@solana-program/token-2022";
// Builds a single instruction; no proofs are needed to apply.
const instruction = getApplyConfidentialPendingBalanceInstructionFromToken({
token: tokenAccountAddress,
tokenAccount, // decoded Token account
authority: owner,
elgamalSecretKey: elgamalKeypair.secret(),
aesKey
});

See the Apply Pending Balance page for the full flow.

Sending transfers

A confidential transfer requires three zero-knowledge proofs generated on the client:

  • Equality proof that the sender's new balance ciphertext encrypts the same value as a fresh commitment the sender can open.
  • Ciphertext validity proof that the transfer-amount ciphertexts are well-formed under the source, destination, and (if set) auditor keys.
  • Range proof that the amount and the sender's remaining balance are valid non-negative integers, which is what prevents minting value out of thin air.

These proofs are larger than today's transaction size limit allows inline, so the usual pattern is to create proof context state accounts, verify each proof into them, reference them from the transfer instruction, then close them to reclaim rent. That spans a few dependent transactions. Transaction format v1 (landing with Agave v4.2) raises the size limit and is expected to let a confidential transfer run in a single onchain transaction, which will simplify this flow.

You do not have to assemble the proofs by hand. Both clients provide a high-level helper that builds the proofs and produces the instructions to submit:

import { getConfidentialTransferInstructionPlan } from "@solana-program/token-2022";
// Returns an instruction plan covering proof setup, the transfer, and cleanup.
const plan = await getConfidentialTransferInstructionPlan({
rpc,
payer, // funds rent for the temporary proof context state accounts
sourceToken,
mint,
destinationToken,
sourceTokenAccount, // decoded Token account for the source
destinationTokenAccount, // decoded Token account for the destination,
// or pass `destinationElgamalPubkey` directly instead
authority: owner,
amount,
sourceElgamalKeypair, // ElGamal keypair for the source account
aesKey, // AES key for the source account
auditorElgamalPubkey // optional, read from the mint config
});
// Execute the plan with your instruction-plan executor of choice.

If you need finer control, the lower-level building blocks are available too: @solana/zk-sdk generates each proof's data (CiphertextCommitmentEqualityProofData, BatchedGroupedCiphertext3HandlesValidityProofData, BatchedRangeProofU128Data), @solana-program/zk-elgamal-proof provides the proof verification instructions, and @solana-program/token-2022 provides the confidentialTransfer and context state account instructions.

See the Transfer Tokens page for the detailed instruction sequence.

Withdrawing

Withdrawing moves funds from the confidential available balance back to the public balance, after which they behave like any normal token balance. Withdrawal also requires proofs (an equality and a range proof), exposed as getConfidentialWithdrawInstructionPlan in the JS client and confidential_transfer_withdraw in the Rust client. Apply any pending balance first so the full amount is available. See Withdraw Tokens.

Two optional extensions build on confidential transfers. Both are mainly the issuer's responsibility, but each changes something an integrator should handle:

  • Confidential transfer fees. When a mint pairs confidential transfers with a transfer fee, sends use a with-fee transfer path (confidential_transfer_transfer_with_fee in the Rust client), and the withheld fee is encrypted like the amount. Collecting withheld fees (harvest and withdraw) is the fee authority's job, not the integrator's.
  • Confidential mint and burn. A mint with this extension issues and burns supply confidentially, which disables the public deposit and withdraw path. Tokens cannot move between the public and confidential balances on such a mint, so do not surface deposit or withdraw for it.

For the protocol-level details of both, see the confidential balances documentation.

Parsing confidential transfer transactions

Explorers and indexers need to recognize and label confidential transfer activity without being able to read the amounts. The @solana-program/token-2022 client exposes identifyToken2022Instruction to classify each Token-2022 instruction, plus per-instruction parsers (for example parseConfidentialTransferInstruction) to decode accounts and non-secret fields. The encrypted amounts stay ciphertexts: display them as confidential rather than rendering the bytes as a number.

JS-parse-instruction.ts
import {
identifyToken2022Instruction,
Token2022Instruction,
TOKEN_2022_PROGRAM_ADDRESS
} from "@solana-program/token-2022";
for (const ix of instructions) {
if (ix.programAddress !== TOKEN_2022_PROGRAM_ADDRESS) continue;
const kind = identifyToken2022Instruction(ix);
switch (kind) {
case Token2022Instruction.ConfidentialTransfer:
case Token2022Instruction.ConfidentialTransferWithFee:
console.log("Confidential transfer (amount encrypted)");
break;
case Token2022Instruction.ConfidentialDeposit:
console.log("Deposit to confidential balance");
break;
case Token2022Instruction.ConfidentialWithdraw:
console.log("Withdraw from confidential balance");
break;
case Token2022Instruction.ApplyConfidentialPendingBalance:
console.log("Apply pending balance");
break;
default:
break;
}
}

Indexing limitations to plan for:

  • No amounts for confidential transfers. A ConfidentialTransfer carries encrypted amounts, so volume, flow, and transfer-size analytics cannot be computed from it. Mark these transfers as confidential rather than recording a zero or a raw ciphertext.
  • No balance deltas. Confidential moves do not change the public token balance, so the pre/post token balance diffing that drives most transfer indexing does not capture them. The pending and available balances are ciphertexts.
  • Deposits and withdrawals are in the clear. ConfidentialDeposit and ConfidentialWithdraw carry cleartext amounts, so you can still index the flow into and out of the confidential pool, just not the transfers within it.
  • One transfer spans several transactions today. Proof context state accounts are created, used, and closed around the transfer, so correlate the related transactions rather than treating each in isolation. This collapses once single-transaction confidential transfers land (see above).
  • Auditor mints are decryptable. If a mint has a global auditor and you hold the auditor key, you can decrypt transfer amounts for that mint (see below).

Auditors and compliance

A mint may configure a global auditor ElGamal public key. When set, every confidential transfer must include the amount encrypted under the auditor key, so the holder of the auditor secret key can decrypt all transfer amounts for that mint. The validity proof covers the auditor ciphertext, so as an integrator you do not do anything special on the send path beyond passing the auditor key from the mint config (the high-level helpers read it for you).

If your product is the auditor (for example a regulated issuer or a compliance provider), you decrypt transfer amounts with the auditor secret key the same way an owner decrypts their own balance. Owners can also selectively share their per-account keys with a specific party without exposing them publicly.

Transaction monitoring (KYT)

For know-your-transaction and AML providers, confidential transfers change what is observable, not the overall model:

  • Address and graph analysis is unaffected. Senders, recipients, mints, and account owners stay public, so address screening, sanctions matching, and counterparty graph analysis work the same as for any token.
  • Amount-based heuristics need the auditor key. Structuring, threshold reporting, and volume-based risk scoring cannot read confidential transfer amounts on their own. Deposit and withdraw amounts stay in the clear, so the size of flows entering and leaving the confidential pool is still visible.
  • Coverage comes from the auditor model. On mints that configure a global auditor, a provider operating with the auditor key (or receiving selective disclosure from the user or issuer) can recover transfer amounts. Issuers in regulated contexts should plan to configure an auditor or support selective disclosure so monitoring is possible.

Backwards compatibility

  • A confidential token account always has a public balance. Wallets and apps that do not support the extension keep working and display the public balance.
  • Standard transfers still function for the public balance as long as the destination allows non-confidential credits.
  • Confidential balances are not visible to non-supporting tooling, but the funds are not lost: any confidential-aware client can access them with the owner's keys.
  • Because amounts are encrypted, supply- and volume-level analytics that rely on reading transfer amounts will not see confidential activity. Plan dashboards and accounting around this.

General Requirements

RequirementDescriptionPriority
Detect the extensionRecognize the Confidential Transfer extension on mints and accounts and handle these tokens explicitly rather than assuming a public-only model.P0
Never lose confidential fundsEven without full support, surface that a confidential balance exists so users do not assume their account is empty.P0
Sound key managementChoose a key strategy for ElGamal and AES keys. Deriving from the owner's wallet is the recommended default; if you store keys, protect them like signing keys.P0

Wallets

RequirementDescriptionPriority
Display public balanceAlways show the public balance using standard token balance reads.P0
Unlock and display available balanceLet the user unlock keys via a signature and show the decrypted available balance (via the AES decryptable balance).P0
Show pending vs availableSurface pending balances separately and prompt to apply when funds are received.P1
Apply pending automaticallyApply pending balances at sensible moments (e.g. before a send) so funds do not get stuck.P1
Send confidentiallySupport deposit, transfer, and withdraw flows with client-side proof generation.P1
Locked-state UXWhen keys are not unlocked, clearly indicate a confidential balance exists rather than showing zero.P1
Onboarding / educationHelp users understand what stays private (amounts and balances) and the key-unlock step.P2

Explorers and Indexers

RequirementDescriptionPriority
Label confidential accounts and mintsClearly mark accounts and mints that use the extension, and show the public balance.P0
Parse confidential instructionsDecode configure, deposit, apply, transfer, and withdraw instructions and display their type (not the amounts).P0
Do not display encrypted amounts as numbersNever render ciphertext fields as if they were plaintext balances; show them as confidential.P0
Index public deposit/withdraw flowsRecord the cleartext amounts on deposit and withdraw to track flow in and out of the confidential pool.P1
Correlate multi-transaction transfersGroup the proof setup, transfer, and cleanup transactions that make up one confidential transfer.P1
Show auditor configurationSurface whether a mint has a global auditor configured.P1
Proof account lifecycleRecognize proof context state account creation and closing so transactions read sensibly.P2

Exchanges and Custodians

RequirementDescriptionPriority
Track public and confidential stateAccount for both public and confidential balances when crediting deposits and computing holdings.P0
Apply pending on depositsApply pending balances when confidential deposits arrive so credited amounts are accurate.P0
Secure key handlingIf holding confidential funds custodially, manage ElGamal/AES keys with the same rigor as signing keys.P0
Account provisioning via registryTo set up confidential accounts for users, have users register an ElGamal key once and provision with the registry path rather than requiring a signature per account.P1
Withdrawals to usersSupport confidential or public withdrawals depending on the destination account configuration.P1
Compliance / auditor supportWhere required, use auditor keys or selective disclosure to meet reporting obligations.P1
Internal accounting on raw amountsReconcile against decrypted amounts at the edge and store them; do not attempt to aggregate ciphertexts.P1

Compliance and KYT Providers

RequirementDescriptionPriority
Address and graph screeningScreen senders, recipients, mints, and owners and run counterparty graph analysis; these stay public.P0
Track public deposit/withdrawMonitor cleartext deposit and withdraw amounts as the observable entry and exit points of the confidential pool.P0
Auditor-key amount visibilityFor auditor-enabled mints, decrypt transfer amounts using the auditor key to support amount-based detection.P1
Selective disclosure intakeSupport amount disclosures shared by users or issuers via per-account keys.P2

Is this page helpful?

© 2026 Solana Foundation. All rights reserved.