Skip to main content

Protocol reference

This page documents the wire format for each message in the Confidential x402 protocol, the ZK circuit, and the privacy and trust model.


Payment schemas

PaymentRequired (402 response)

The resource server encodes this object as base64 and sends it in the body of its 402 response.

{
"x402Version": 2,
"resource": {
"url": "/api/premium-data",
"description": "Premium data endpoint"
},
"accepts": [
{
"scheme": "confidential",
"network": "eip155:84532",
"amount": "1000000",
"asset": "0xUSDC...",
"payTo": "0xResourceServer...",
"maxTimeoutSeconds": 300,
"extra": {
"confidentialToken": "0xMercesContract...",
"eip712Domain": {
"name": "Merces",
"version": "1"
},
"mpcPks": [
["<x1>", "<y1>"],
["<x2>", "<y2>"],
["<x3>", "<y3>"]
]
}
}
]
}
FieldTypeDescription
x402VersionnumberAlways 2
schemestringAlways "confidential"
networkstringCAIP-2 chain identifier, e.g. "eip155:84532"
amountstringRequired payment in token base units (e.g. USDC with 6 decimals)
assetstringERC-20 token contract address
payTostringResource server's receiving wallet address
maxTimeoutSecondsnumberMaximum age of a valid payment signature in seconds
extra.confidentialTokenstringMerces contract address
extra.eip712DomainobjectEIP-712 domain name and version for signing
extra.mpcPks[[x,y], ...]BabyJubJub public keys of the three MPC operators

PaymentPayload (client → server)

The client encodes this object as base64 and sends it in the PAYMENT-SIGNATURE header of its retried request.

{
"x402Version": 2,
"resource": { "url": "/api/premium-data" },
"accepted": {
"scheme": "confidential",
"network": "eip155:84532",
"amount": "1000000",
"asset": "0xUSDC...",
"payTo": "0xResourceServer...",
"maxTimeoutSeconds": 300,
"extra": { "...": "..." }
},
"payload": {
"signature": "0x<eip712-signature>",
"authorization": {
"from": "0xClient...",
"to": "0xResourceServer...",
"amountCommitment": "<poseidon2-commitment>",
"amountR": "<r>",
"beta": "<public-input>",
"ciphertexts": ["<c0>", "<c1>", "<c2>", "<c3>", "<c4>", "<c5>"],
"senderPk": ["<x>", "<y>"],
"nonce": "0x<32-bytes>",
"deadline": "<unix-timestamp>",
"proof": {
"pi_a": ["<x>", "<y>", "1"],
"pi_b": [["<x0>", "<x1>"], ["<y0>", "<y1>"], ["1", "0"]],
"pi_c": ["<x>", "<y>", "1"],
"protocol": "groth16",
"curve": "bn254"
}
}
}
}
FieldTypeDescription
payload.signature0x...EIP-712 signature over the authorization struct
authorization.fromaddressClient's wallet address (token owner)
authorization.toaddressResource server's wallet address (recipient)
authorization.amountCommitmentFrPoseidon2 commitment to (amount, blindingFactor)
authorization.amountRFrblindingFactor for amount commitment
authorization.betaFrPublic input derived during proof generation
authorization.ciphertextsFr[6]Six BN254 field elements: two encrypted shares per MPC operator
authorization.senderPk[x, y]Ephemeral BabyJubJub public key used for ECDH encryption
authorization.noncebytes32Unique 32-byte nonce for replay protection
authorization.deadlineuintUnix timestamp after which the signature is invalid
authorization.proofGroth16ProofGroth16 proof in snarkjs format

EIP-712 typed data

The client signs the following typed data. The verifyingContract is the Merces contract address provided in extra.confidentialToken.

{
"domain": {
"name": "Merces",
"version": "1",
"chainId": 84532,
"verifyingContract": "0xMercesContract..."
},
"types": {
"TransferFromAuthorization": [
{ "name": "sender", "type": "address" },
{ "name": "receiver", "type": "address" },
{ "name": "amountCommitment", "type": "uint256" },
{ "name": "ciphertextHash", "type": "bytes32" },
{ "name": "beta", "type": "uint256" },
{ "name": "nonce", "type": "uint256" },
{ "name": "deadline", "type": "uint256" }
]
},
"primaryType": "TransferFromAuthorization"
}

ciphertextHash is keccak256(abi.encode(ciphertexts, senderPk)).

Facilitator API

POST /verify

Called by the resource server before serving content.

Request:

{
"x402Version": 2,
"paymentPayload": { "...": "..." },
"paymentRequirements": { "...": "..." }
}

Response (success):

{ "isValid": true, "payer": "0xClient..." }

Response (failure):

{ "isValid": false, "invalidReason": "insufficient_funds" }

POST /settle

Called by the resource server after content has been served.

Request: Same structure as /verify.

Response:

{
"success": true,
"transaction": "0xTxHash...",
"network": "eip155:84532"
}

ZK circuit

The Groth16 proof is generated over the BN254 curve and proves four simultaneous claims:

ClaimDescription
Commitment correctnessamountCommitment = Poseidon2([amount, blindingFactor])
Share validityThree additive shares sum to amount modulo the BN254 scalar field order
Encryption correctnessEach share is correctly encrypted to the stated MPC operator key via BabyJubJub ECDH
Rangeamount fits in 80 bits

Public inputs (15 elements):

IndexValue
0beta
1–2senderPk.x, senderPk.y
3amountCommitment
4–9ciphertexts[0..5]
10–11mpcPk[0].x, mpcPk[0].y
12–13mpcPk[1].x, mpcPk[1].y
14–15mpcPk[2].x, mpcPk[2].y

Private inputs: amount, blindingFactor, three plaintext shares, three blinding shares, BabyJubJub ephemeral secret key.


Privacy & trust model

What is protected

DataStandard x402Confidential x402How
Transfer amountsPlaintext in transferWithAuthorizationPoseidon2 commitment onchainReal amount secret-shared across MPC network
Account balancesPublic via ERC-20 balanceOf()Only a commitment stored onchainActual balances held as secret shares by MPC operators
Spending patternsFull payment history enables behaviour profilingObservers can count payments but cannot sum amountsAmounts hidden; only frequency and counterparties visible
Price discrimination evidenceOnchain proof of what each user paidNo onchain evidence of negotiated priceResource server holds client payment data offchain

What is not protected

Onchain:

Leaked dataSourceImpact
Sender addressActionQuery.senderAnyone can see who sends payments
Receiver addressActionQuery.receiverAnyone can see who receives payments
Payment frequency and timingtransferFrom() call eventsObservers can count API calls and when they occur
Nonce consumptionusedNonces mappingNumber of nonces reveals payment count per sender
Sender–receiver graphAggregated from all transfersCan map which clients use which APIs

HTTP layer:

Leaked dataWho can see it
Required payment amountResource server (sets it), facilitator; transmitted over TLS to client
API endpoint pathResource server, facilitator
Client wallet addressResource server, facilitator

The amount in the PaymentRequired response is visible to the client over TLS but is not written to the blockchain. A passive observer without TLS access cannot learn the amount.

Trust model

MPC network

Privacy of payment amounts and balances depends on the MPC operators not colluding. The current deployment uses REP3 secret sharing: where a majority of operators must cooperate to reconstruct any amount or balance. If two operators are honest, no amount is revealed. TACEO operates all three nodes in the current testnet deployment.

Facilitator

The facilitator is a liveness dependency, not a privacy dependency. It can refuse to verify or settle payments, and it can observe which addresses are paying which resource servers. It cannot learn balances, forge valid proofs or signatures, or steal funds.

Merces contract

The contract is the trust anchor for correctness. It verifies the client's Groth16 proof before enqueuing any transfer, verifies the MPC's proof before committing balance updates, and enforces nonce uniqueness and deadline expiry.

Roadmap to stronger privacy

The current confidential scheme hides amounts and balances. Sender and receiver addresses remain visible onchain. Future versions will include an enhanced variant that additionally hide the sender–receiver relationship, building on Merces' existing support for fully private transactions.