In our previous post, we made the case for why x402 needs a privacy layer: the average agent payment is $0.31, the legacy rails cannot service it, and the moment B2B pricing moves onchain it becomes a public broadcast. We also introduced Merces, our confidential token transfer system, and the confidential scheme we built on top of it.
Here we describe the architecture, the cryptographic primitives, the ZK circuit, the protocol end-to-end, and an honest comparison with every serious alternative we have found. The implementation is live on Base Sepolia and available on GitHub.
Architecture at a Glance
Five components cooperate to complete a confidential x402 payment.
The Client is the agent or user-side library. It holds the spending key, maintains local balance state, generates the ZK proof, and constructs the payment payload.
The Resource Server is the API provider. It is unchanged from standard x402 in structure: it issues HTTP 402 responses, forwards payment payloads to a facilitator, and serves content on successful verification. The only difference is that it advertises scheme: "confidential" in its payment requirements.
The Facilitator is the off-chain verifier and settlement router. It checks ZK proofs via snarkjs, queries the MPC network for balance sufficiency, validates EIP-712 signatures, and submits settlement transactions. It has the same interface contract as a stock x402 facilitator, with a verifier for the new scheme plugged in.
The MPC Network is a committee of operators that holds the secret-shared balances. It answers yes/no affordability queries, decrypts the amount shares during settlement, and produces its own ZK proof that balance updates are correct.
The PrivateBalance contract lives on Base. It stores balance commitments, not balances. It verifies the client Groth16 proof onchain during transferFrom(), enqueues the MPC action, and verifies the MPC’s proof when the balance update is finalized.
The Cryptographic Stack
- Groth16 over BN254 for proofs.
- Poseidon2 for commitments over amounts and balances.
- BabyJubJub ECDH for in-circuit encryption of each share to an MPC operator’s public key.
- Secret sharing of each payment amount across the MPC operators.
- EIP-712 signatures binding each proof to a specific sender, receiver, and nonce.
The ZK Circuit
→ TACEO Confidential x402 GitHub repository
In a single Groth16 proof, the client demonstrates four facts simultaneously:
- Commitment correctness. The published Poseidon2 commitment is the hash of the true payment amount plus a blinding factor.
- Share validity. Three additive shares have been produced whose sum equals the committed amount, modulo the BN254 scalar field order.
- Encryption correctness. Each share is correctly encrypted to the stated operator public key under BabyJubJub ECDH.
- Range. The amount fits in 80 bits.
Public inputs: the commitment, the three ciphertexts, the three operator public keys, sender address, receiver address, nonce.
Private inputs: the amount, the blinding factor, the three plaintext shares, the BabyJubJub ephemeral secrets.
Performance, measured against the deployed circuit:
- Proving: ~400ms end-to-end
- Off-chain verification: ~3ms
- Onchain verification: ~300,000 gas
Protocol Walkthrough, End to End
Phase 1: Discovery (Client → Resource Server)
- Client sends HTTP request to a protected endpoint:
GET /api/premium-data HTTP/1.1api.endpoint.com(as an example)
- Resource server checks config, sees it is protected and that there is no payment-signature header, and returns 402:
HTTP/1.1 402 Payment RequiredPAYMENT REQUIRED: <base64-encoded PaymentRequired>
Payment-required response format:
{
"x402Version": 2,
"resource": {
"url": "/api/premium-data",
"description": "Premium data endpoint"
},
"accepts": [
{
"scheme": "confidential",
"network": "eip155:8453",
"amount": "100000",
"asset": "0xUSDC...",
"payTo": "0xResourceServer...",
"maxTimeoutSeconds": 30,
"extra": {
"confidentialToken": "0xConfToken...",
"eip712Domain": {
"name": "Merces",
"version": "1"
},
"mpcPks": [
["pk0X","pk0Y"],
["pk1X","pk1Y"],
["pk2X","pk2Y"]
]
}
}
]
}
Phase 2: Payment Construction (Client-Side)
- Generate blinding factor .
- Generate a Groth16 proof which proves:
- Poseidon2 commitment matches (amount, ).
- Additive 3-of-3 secret shares sum correctly.
- BabyJubJub ECDH encryption of each share to the corresponding MPC pubkey.
- Amount fits in 80 bits.
- Ephemeral public key is derived correctly from the secret key.
- Create
CipherText, obtain the MPC pubkeys fromextra.mpcPublicKeys:
ciphertext = {
amount: [share1, share2, share3], // secret shares of val
r: [rShare1, rShare2, rShare3], // secret shares of randomness r
sender_pk: { x, y } // ephemeral BabyJubJub pub key
}
-
Create EIP-712 typed-data message containing:
sender— bind payment to client address.receiver— ensure it only goes here, not somewhere else.amountCommitment—Poseidon2([val, r], 0x..).keccak256(CipherText).nonce— replay protection.deadline.
-
Package payload to be sent:
{
"x402Version": 2,
"resource": { "url": "/api/premium-data" },
"accepted": {
"scheme": "confidential",
"network": "eip155:8453",
"amount": "val..",
"asset": "0xUSDC...",
"payTo": "0xResourceServer...",
"maxTimeoutSeconds": 30,
"extra": { }
},
"payload": {
"signature": "0xSignature",
"authorization": {
"from": "0xClient...",
"to": "0xResourceServer..",
"amountCommitment": "<poseidon2 hash>",
"amountR": "<r>",
"beta": "...",
"ciphertexts": ["...", "...", "...", "...", "...", "..."],
"senderPk": ["...","..."],
"proof": {
"pi_a": ["...", "..."],
"pi_b": [["...", "..."], ["...", "..."]],
"pi_c": ["...", "..."],
"protocol": "groth16",
"curve": "bn128"
},
"deadline": "...",
"nonce": "..."
}
}
}
- Resend the original request to the protected endpoint with the payment attached:
GET /api/premium-data HTTP/1.1api.endpoint.comPAYMENT-SIGNATURE: <base64 of PaymentPayload>
Phase 3: Verification (Resource Server → Facilitator)
- Resource server receives and decodes
Payment-Signature. - Resource server checks: does
payload.acceptedmatch one of its ownacceptsentries? - Resource server sends a verify request to the Facilitator:
POST /verify
{
"x402Version": 2,
"paymentPayload": { },
"paymentRequirements": {
"scheme": "confidential",
"network": "eip155:8453",
"amount": "100000",
"asset": "0xUSDC...",
"payTo": "0xResourceServer..."
}
}
- Facilitator routes to the “confidential” scheme handler and runs
verify():- Check ciphertext —
senderPkis on BabyJubJub and all shares are in the prime field. - Check sufficient balance for sender — query MPC network: “does sender have ≥ val (amount needed to pay resource server for query)”.
- Check validity of signed typed data — signed payload by client covering
amountCommitment, deadline, nonce, etc. - Check commitment matches amount — recompute
commit(amount, blindingFactor)off-chain and verify it equalsamountCommitment. - Check client ZK proof — verify the Groth16 proof off-chain against 15 public signals constructed from (senderPk, amountCommitment, ciphertext shares interleaved, mpcPublicKeys).
- Check ciphertext —
- Facilitator returns a
verify()response sayingvalidfor0xClient...
Phase 4: Serve Content & Settlement (Resource Server → Facilitator → Contract → MPC)
- Resource server sends settle request to facilitator:
POST /settle
{
"x402Version": 2,
"paymentPayload": { },
"paymentRequirements": { }
}
The facilitator sends the ciphertext (encrypted amount + sender address) to the MPC network before settling. Each MPC operator decrypts its share, reconstructs the amount, compares against the sender’s real balance, and they collectively return a signed {sufficient: true/false}. The facilitator sees only yes/no and then settles.
- Facilitator calls
transferFrom()on the confidential token contract:
confToken.transferFrom(
0xClient, // sender: Client address
ClientAuth, // Signature from client to execute this transfer
nonce, // Unique
deadline, // Did this come in the required window
0xResourceServer, // receiver (payTo)
amountCommitment, // Poseidon2 commitment
ciphertext, // 3 secret shares + ephemeral pk
clientProof // Groth16 proof { pA, pB, pC }
)
- The contract validates inputs (commitment in field, pk on curve, shares in field), verifies the Groth16 client proof onchain via
ClientTransferVerifier.verifyProof(pA, pB, pC, pubSignals)wherepubSignalsis a 15-element array built from(senderPk, amountCommitment, interleaved ciphertext, stored mpc_pk1/pk2/pk3). If the proof is invalid, it reverts withInvalidProof(). Otherwise it enqueues the transfer action. - Facilitator returns a settle response with tx hash:
{
"success": true,
"transaction": "0xTxHash...",
"network": "eip155:8453"
}
- MPC picks up the transfer action.
- MPC decrypts the secret shares and recovers the actual amount (val) and randomness (r). It verifies:
Poseidon2([val, r], 0x..) == amountCommitment. - MPC updates balances.
- MPC generates a Groth16 proof showing the balance update is valid.
- MPC calls
processMPC(inputs, proof)on the contract. The contract verifies the proof, updates balance commitments for both parties, and removes the action from the queue. - Resource server receives the facilitator’s verification and serves content to the client:
HTTP/1.1 200 OKContent-Type: application/jsonPAYMENT-RESPONSE: <base64 of SettleResponse>
Privacy Analysis
What is protected
| What | Standard x402 (exact) | Confidential x402 | How |
|---|---|---|---|
| Transfer amounts | Plaintext in transferWithAuthorization call | Poseidon2 commitment onchain | Real amount secret-shared with MPC network |
| Account balances | Public via ERC-20 balanceOf() | Confidential token contract only exposes a commitment | Actual balances only retrievable by MPC network via secret sharing |
| Spending patterns | Full payment history enables behavior profiling of API consumers | History only shows who interacted with whom; behavior profiling more limited | Observers can count payments but cannot sum amounts |
| Price discrimination evidence | Onchain proof of what each user paid | No onchain evidence; the resource server (API provider) maintains all client payment data | Cannot prove different users paid different prices for the same API |
Onchain visibility
| Leaked data | Where | Impact |
|---|---|---|
| Sender address | ActionQuery.sender | Anyone can see who sends |
| Receiver address | ActionQuery.receiver | Anyone can see who receives |
| Payment frequency and timing | transferFrom() calls | Observers can count API calls a client makes and when |
| Nonce consumption | usedNonces mapping in contract | Number of nonces reveals payment count per sender |
| Sender-receiver graph | Aggregated from all transfers over time | Can map which clients use which APIs |
HTTP-layer visibility
| Leaked data | Where | Who can see it | Changed in Confidential x402? |
|---|---|---|---|
| Service price (amount) | PaymentRequirements.amount and PaymentPayload.accepted.amount | Resource server (sets it); protected via TLS with client, facilitator | Yes |
| API endpoint | resource.url | Resource server, facilitator | No |
| Client wallet address | payload.authorization.sender | Resource server, facilitator | No |
Roadmap to Full Privacy
Merces already supports fully private transactions in its standalone form. The broader Merces protocol encrypts sender, receiver, and amount, building a private transaction graph on top of the same cryptographic stack described above.
We’re working on integrating that capability into x402: extending the confidential scheme with a private variant that uses stealth addresses and additional in-circuit checks on the relationship between commitments across transactions.
Try It, Build On It
The full implementation is open source.
The repo contains the contracts, the facilitator, the client library (Rust + TypeScript libs), the ZK circuit, the MPC network, and an end-to-end demo where a user makes real USDC payments on Base Sepolia with hidden amounts.
Merces integrates with x402 as a standard scheme. If you are already running an x402 facilitator or resource server, adding confidential payments is a scheme registration.
Additional resources:
- x402 Rust repository: github.com/x402-rs/x402-rs
- x402 foundation repository: github.com/x402-foundation/x402
We want to hear from you! Issues, pull requests, and threat-model critiques are welcome on GitHub.
For partnership, integration, or research conversations, get in touch about a Merces design partnership.