Solana confidential transfers are back. x402 cannot see them.
Solana turned encrypted stablecoin transfers back on in June, and four days ago it made them fit in a single transaction. Every x402 facilitator on SVM verifies payments by reading an amount that is no longer there.
The x402 exact scheme for Solana is built on one assumption: a facilitator can look at a signed transaction, find the transfer, and read the number. We walked through that machinery in x402 on Solana and through the verify/settle contract itself in the facilitator audit. The assumption held because every stablecoin transfer on Solana carried its amount in plaintext instruction data.
Token-2022's confidential transfer extension breaks that assumption on purpose. It encrypts balances and transfer amounts under twisted ElGamal, proves correctness with zero-knowledge proofs, and leaves the chain with a transfer whose size nobody can read. It was switched off across the network in June 2025 after a bug in the proof verifier. It is on again. This is an audit of what came back, what it costs, and exactly where it collides with x402.
Reading the feature gates off mainnet
Do not take a blog's word for whether a Solana feature is live — including this one. Feature gates are ordinary accounts owned by Feature111111111111111111111111111111111111, holding nine bytes: a one-byte Option tag and a little-endian u64 activation slot. Agave declares the two relevant gates in its feature-set crate as disable_zk_elgamal_proof_program and reenable_zk_elgamal_proof_program.
# the re-enable gate
curl -s https://api.mainnet-beta.solana.com -X POST \
-H 'content-type: application/json' \
-d '{"jsonrpc":"2.0","id":1,"method":"getAccountInfo",
"params":["zkexuyPRdyTVbZqEAREueqL2xvvoBhRgth9xGSc1tMN",
{"encoding":"base64"}]}'
# data: "AQAlSRkAAAAA" -> tag=1, slot=424224000
Decoded against getBlockTime, the timeline is unambiguous. The disable gate zkdoVwnSFnSLtGJG7irJPEYUpmb4i7sGMGcnN6T9rnC activated at slot 347,760,000 — the first slot of epoch 805, 2025-06-19T06:07:13Z. The re-enable gate activated at slot 424,224,000, the first slot of epoch 982, 2026-06-04T10:18:07Z. The disable gate has not been used since.
The gate alone was not enough: Token-2022 itself had been patched to reject confidential instructions. Its upgradeable loader record shows the program was last redeployed at slot 427,147,035, 2026-06-17T20:57:58Z, two weeks after the runtime gate. That is the date the extension actually became usable end to end, and Solana's Confidential Balances documentation no longer carries a disabled warning.
What a confidential transfer looks like today
The second date matters as much as the first. SIMD-0385 raises the maximum transaction size from 1,232 to 4,096 bytes and folds compute-budget configuration into a header bitmask. Its gate, txv1aq4pp281K9um3tnPgkfX8UqtFT6wcVW3hNezGLL, activated at slot 447,120,000 — epoch 1035, 2026-09-15T01:04:23Z. Four days ago.
That is the difference between a confidential transfer being an orchestration problem and being a transaction. The proofs are large; under the old limit they had to be verified in separate transactions that wrote into temporary proof context state accounts, which the token program then read and which someone had to close afterwards to reclaim rent. Three or four dependent transactions, each a failure point for an agent to retry.
Here is one taken from mainnet this morning, version 1, 2,687 bytes, 289,983 compute units, 10,000 lamports in fees. The instruction log reads in full:
Program log: Instruction: TakePrivateLink
ConfidentialTransferInstruction::ApplyPendingBalance 7,968 CU
ConfidentialTransferInstruction::Transfer 15,363 CU
ConfidentialTransferInstruction::EmptyAccount 1,835 CU
Instruction: CloseAccount 1,724 CU
Program ZkE1Gama1Proof111...: VerifyCiphertextCommitmentEquality
Program ZkE1Gama1Proof111...: VerifyBatchedGroupedCiphertext3HandlesValidity
Program ZkE1Gama1Proof111...: VerifyBatchedRangeProofU128
Program ZkE1Gama1Proof111...: VerifyZeroCiphertext
Read the cost structure. The token program's own work is trivial — the Transfer instruction consumed 15,363 CU. The application program consumed 61,183 CU for everything it did. The remaining ~229,000 CU, roughly four fifths of the transaction, is the runtime verifying four zero-knowledge proofs: an equality proof that the sender's new balance ciphertext encrypts the same value as a fresh commitment, a grouped validity proof that the amount ciphertexts are well-formed under the source, destination and optional auditor keys, a batched range proof that no amount underflows, and a zero-ciphertext proof for closing the account.
The matching send, in the other direction, is denser still: 2,945 bytes, 356,204 CU, 15,000 lamports, and a sequence that tells you everything about the current state of the rail.
Instruction: Wrap // SPL Token -> Token-2022
Instruction: MintTo
ConfidentialTransferInstruction::Deposit
ConfidentialTransferInstruction::ApplyPendingBalance
Instruction: Reallocate // "account needs resize, +300 bytes"
ConfidentialTransferInstruction::ConfigureAccount
ConfidentialTransferInstruction::Transfer
The sender had to wrap a classic SPL token into a Token-2022 mint first, because the token it actually holds cannot do this. Then deposit into the confidential pending balance, apply it, grow the recipient account by 300 bytes, configure it with an ElGamal public key, and only then transfer. Seven steps, one transaction, because of a feature gate that is four days old.
Why the x402 exact scheme cannot verify it
Now put that transaction in front of a facilitator. The exact scheme for SVM is explicit about what constitutes a payment:
The transaction MUST result in a transfer of at leastPaymentRequirements.amount, of the asset identified byPaymentRequirements.asset, to the recipient identified byPaymentRequirements.payTo(the Associated Token Account derived frompayToandasset), using one of:spl-tokenTransferChecked,token-2022TransferChecked.
The spec then requires that a verifier deterministically confirm the correct mint, destination account, amount and token program, and that exactly one transfer matching those requirements appears across top-level instructions and the full CPI trace. Zero matches is a rejection. Two matches is a rejection.
A confidential transfer fails all three legs of that test, and it fails them structurally rather than accidentally.
There is no TransferChecked
ConfidentialTransferInstruction::Transfer is a different instruction with a different discriminator. The reference verifier's static path scans for a TransferChecked at the top level; the smart-wallet path simulates with innerInstructions: true and scans the CPI trace for the same thing. Neither finds one. The transaction is rejected with "no matching transfer", which is the correct outcome for the wrong reason — the payment may well have happened.
There is no amount to compare
Even if the matcher learned the new discriminator, the instruction data carries ciphertexts and proof references, not a u64. There is no comparison to make against PaymentRequirements.amount. The facilitator cannot distinguish a correct payment from a payment of one base unit, which is precisely the property the extension was built to provide.
The balance delta is zero
The spec's post-settlement defence against TOCTOU is a fallback: if inner instructions are unavailable, compare the destination ATA's balance before and after and require balanceAfter - balanceBefore >= required. In the send transaction above, the public token balance of both parties after execution is 0. The value sits in the encrypted available balance. A balance-delta check on a confidential transfer returns zero for a payment of any size.
This is not a bug in x402. It is the direct consequence of an outcome-based scheme defined over publicly readable state meeting a token extension whose purpose is to make that state unreadable. Any verification layer built on the same assumption — indexers, accounting exports, the dispute flows in the x402 extensions layer — inherits the same blind spot.
What stays public anyway
Before designing around it, be precise about how much privacy is actually on offer. The documentation is blunt: only transfer amounts and token balances are private; token account addresses remain public. Sender, receiver, timestamp and the shape of the traffic are all still on chain. For an agent that pays the same gateway 400 times a day, the counterparty graph is the interesting signal, and confidential transfers do not touch it.
The boundaries leak too. Deposit and Withdraw move value between the public and confidential halves of the same account, and their amounts are plaintext. One of the transactions in our sample is a Withdraw that parses cleanly to amount: 10000, decimals: 6 — one cent, in the clear. An agent that tops up a confidential balance and drains it per invoice has published both numbers and hidden only the middle.
Then there is the auditor key. A mint may carry a global auditor ElGamal public key; when set, every confidential transfer must include the amount encrypted under it, and the grouped validity proof is what forces all three ciphertexts to encrypt the same value. The holder of the auditor secret key reads every amount for that mint. It is rotatable — UpdateMintData in the Token-2022 interface carries both auto_approve_new_accounts and a new auditor_elgamal_pubkey — and rotation applies to future transfers only.
The liquidity is not there yet
The interesting question is not whether this works but whether anyone is using it. We read the mint accounts directly and parsed the TLV extension list.
PYUSD 2b1kV6Dk... token-2022 supply 726,572,154.38
ConfidentialTransferMint auto_approve=false auditor=none
authority 2apBGMsS6ti9RyF5TwQTDswXBWskiJP2LD4cUEDqYJjk
USDG 2u1tszSe... token-2022 supply 625,306,945.83
ConfidentialTransferMint auto_approve=false auditor=none
authority 2apBGMsS6ti9RyF5TwQTDswXBWskiJP2LD4cUEDqYJjk
USDC EPjFWdd5... spl-token 82-byte mint, no extensions
USDT Es9vMFrz... spl-token 82-byte mint, no extensions
EURC HzwqbKZw... spl-token 82-byte mint, no extensions
Two things stand out. First, the two largest Token-2022 stablecoins on Solana have the extension initialized but with auto_approve_new_accounts set to false, and both point at the same confidential-transfer authority. On those mints an agent cannot self-serve: after ConfigureAccount the account must be approved by that authority before it can be used. Permissioned privacy is a product decision an issuer is entitled to make, and it is also a hard dependency in an autonomous payment path.
Second, USDC, USDT and EURC on Solana are plain 82-byte SPL Token mints. They cannot do confidential transfers at all. The only route is wrapping into a Token-2022 mint, which is exactly what we saw: the destination mint in our sample transaction is a wrapped mint created by the SPL Token Wrap program pWrapnbzNPTx9aZPAp3gpxAUrs3H4QQ1GHWMPMbDba2, and its backpointer PDA records the unwrapped mint as Es9vMFrz… — USDT.
Its total supply is 1,190,000 base units. 1.19 USDT. A second confidential-capable mint in the same sample holds 0.042 units. Over the public RPC's available index window on 2026-09-19 — 2 hours and 12 minutes, 03:49 to 06:01 UTC — exactly 36 transactions touched the ZK ElGamal proof program, none of them failed, and the ones we could decode were four Transfers, four ConfigureAccounts and one Withdraw, all driven by a single application program. None involved PYUSD or USDG.
So: the cryptography is live, the ergonomics just improved by an order of magnitude, the issuers have the switch installed but not flipped, and the wrapped liquidity actually moving is about a dollar. This is what a rail looks like four days after the constraint that made it painful was removed.
Three ways to verify a payment you cannot read
If confidential settlement is where stablecoin payments eventually go — and there are real reasons for an enterprise to not publish its per-call inference spend — then x402 needs a verification story that does not depend on reading the amount. Three are available today, in increasing order of how much they give up.
Hand the facilitator an auditor key. The cleanest fit with the existing spec: the facilitator holds the auditor secret for the mint, decrypts the amount, and runs the same comparison it runs now. The problem is granularity. The auditor key is per mint, not per merchant, so an agent that pays through that facilitator on that mint exposes every amount to it. That is privacy from the public, not from the counterparty — a reasonable trade for regulated flows, useless as a general default.
Settle in public, hold in private. Pay with an ordinary TransferChecked, exactly as today, and use confidential balances only for treasury between payments. Verification is unchanged, the extension does real work — an observer sees the payments but not the agent's runway — and the leak is the per-transaction amount, which for metered inference is already the least sensitive number. This is the only option that requires no protocol change at all.
Move the assertion off chain. x402 already has an offer-and-receipt extension in which the resource server issues a signed receipt with version, network, resourceUrl, payer, issuedAt and an optional transaction. Its design note is that receipts are minimal by default to reduce correlation risk — which is the same instinct as confidential transfers, applied one layer up. A payee that can decrypt its own incoming amount is the natural issuer of an attestation that the amount was correct. The facilitator stops proving the payment and starts relaying a claim about it, which is a genuinely weaker guarantee and should be labelled as such.
What it means for LLM4Agents
The gateway sits on the verification side of this, not the payment side. Our SVM path derives a destination ATA, matches exactly one TransferChecked, and compares an amount. That code is correct today and will stay correct: nothing about confidential transfers changes how a public transfer verifies. The exposure is narrower and more specific.
The first concrete item is error quality. An agent that pays with a Token-2022 confidential transfer today gets a generic "no matching transfer" rejection, which reads like a client bug and will cost someone an afternoon. A facilitator that recognizes ConfidentialTransferInstruction::Transfer in the trace and returns a distinct, honest reason — unsupported: confidential amount, x402 exact requires a readable transfer — turns a debugging session into a one-line fix. Recognizing an instruction is not the same as supporting it, and it is a few hours of work.
The second is the accounting boundary. Everything downstream of settlement in a metered gateway — usage reconciliation, the upto refund path we audited in prompt caching versus metered billing, dispute evidence — assumes the settled amount is readable from chain state. If any of that reconciliation is ever pointed at a confidential rail, it silently reads zero rather than failing. Assumptions that return a plausible wrong answer are worse than assumptions that crash.
The third is strategic, and it rhymes with the argument in TEE-attested agent wallets. Privacy for agent payments is not one feature, it is a stack: confidential amounts on chain, an attested execution environment that holds the keys, and receipts that reveal only what the counterparty needs. The amount layer just came back online after fifteen months. It is worth understanding before a customer asks whether their per-call spend is public, because the honest answer today is yes.
Staying on the frontier
Concrete steps, in the order they pay off.
1. Add confidential-transfer detection to the SVM verifier. Detect the Token-2022 confidential transfer discriminators in the top-level and CPI trace, and return a specific error code instead of the generic no-match. Cheap, defensive, immediately useful. Do the same for the case where the destination ATA balance delta is zero but a Token-2022 confidential instruction is present — that combination is diagnostic.
2. Pin the token-program assumption in tests. Write a regression test that asserts the verifier rejects a confidential transfer rather than accepting it through some balance-delta path. The failure mode to guard against is not rejection, it is a fallback that reads zero and calls it a match.
3. Watch the two gates, not the blog posts. reenable_zk_elgamal_proof_program and enable_tx_v1 are queryable in one RPC call each. A weekly job that decodes both — and re-reads the PYUSD and USDG mints for a flip of auto_approve_new_accounts or a newly set auditor key — is a five-line script and the earliest possible signal that an issuer has decided confidential stablecoin payments are ready.
4. Track transaction v1 for reasons unrelated to privacy. A 4,096-byte transaction limit changes what fits atomically on SVM generally: bigger CPI traces, more instructions alongside a payment, richer smart-wallet flows within the single-transaction envelope the exact scheme verifies. The simulation-based verification path should be re-measured against v1 transactions before someone else discovers the edge case.
5. Decide the privacy posture before it is asked for. The realistic near-term answer for an inference gateway is settle-in-public, hold-in-private: no protocol change, no auditor key, no permissioned mint dependency. Say so explicitly, and be ready to revisit it the day a major issuer sets auto_approve_new_accounts to true.
Payments an agent can verify, on a gateway that explains its rejections
OpenAI-compatible routing, x402 settlement, and error codes that name the real reason.
Register an agent