u402 — Private Agent Payments

Protocol Specification

Formal u402 protocol specification — message formats, verification algorithm, settlement flow, and proof system dispatch.

u402 Protocol Specification

Version: 0.1.0-draft

Status: Draft

Depends on: x402, UPP, UPC


1. Overview

u402 defines a "private" payment scheme for the x402 protocol. When a server returns HTTP 402 with scheme: "private", the client generates zero-knowledge proofs against the Universal Private Pool and returns them in the payment header. The server settles the proofs on-chain without learning the payer's identity.

1.1 Design Goals

  • No key transmission. The client's private keys never leave the client. Only ZK proofs are transmitted.
  • Proof-as-authorization. The ZK proof replaces the EIP-3009 signature. It simultaneously proves note ownership, balance sufficiency, and (optionally) ASP compliance.
  • Proof-system agnostic. The protocol supports multiple proof systems (PLONK/BLS12-381, Circle STARK/M31) through a proofSystem discriminator.
  • x402 compatible. u402 follows x402 v2's plugin architecture. Servers can accept both "exact" (plain x402) and "private" (u402) schemes simultaneously.

2. Payment-Required (Server → Client)

When a server requires private payment, it returns HTTP 402 with a Payment-Required header containing:

{
  "scheme": "private",
  "network": "eip155:11155111",
  "pool": "0x3b5ed60adbd5dd57f47ec54179e4b1c06636285b",
  "asset": "0xTokenAddress",
  "payTo": "0xServiceAddress",
  "amount": "1000000000000000000",
  "aspId": 1,
  "acceptedProofSystems": ["plonk", "circle-stark"]
}

2.1 Field Definitions

FieldTypeRequiredDescription
schemestringYesMust be "private"
networkstringYesCAIP-2 chain identifier (e.g., "eip155:11155111" for Sepolia)
pooladdressYesUPP contract address
assetaddressYesERC-20 token address (must be shieldable in the pool)
payToaddressYesWithdrawal recipient — the service provider's public address
amountstringYesMinimum payment in wei (base units of the token)
aspIduint256YesRequired ASP ID for compliance. 0 = ragequit (no compliance required)
acceptedProofSystemsstring[]NoProof systems the server will accept. Default: ["plonk"]

3. Payment-Signature (Client → Server)

The client generates two ZK proofs — a transfer (split the agent's note into a payment note and change) and a withdraw (convert the payment note to a public ERC-20 transfer to the service provider).

{
  "scheme": "private",
  "proofSystem": "plonk",
  "transfer": {
    "proof": { ... },
    "nullifier": "0x...",
    "stateRoot": "0x...",
    "aspRoot": "0x...",
    "token": "0xTokenAddress",
    "outputCommitment1": "0x...",
    "outputCommitment2": "0x...",
    "encryptedNote1": "0x...",
    "encryptedNote2": "0x..."
  },
  "withdraw": {
    "proof": { ... },
    "nullifier": "0x...",
    "stateRoot": "0x...",
    "aspRoot": "0x...",
    "aspId": 1,
    "token": "0xTokenAddress",
    "amount": "1000000000000000000",
    "recipient": "0xServiceAddress",
    "isRagequit": false
  }
}

3.1 PLONK Proof Format

When proofSystem is "plonk", the proof object contains:

FieldTypeDescription
A, B, C, Zbytes (128 each)G1 polynomial commitments
T1, T2, T3bytes (128 each)Quotient polynomial commitments
eval_a, eval_b, eval_cuint256Wire evaluations at challenge point
eval_s1, eval_s2uint256Permutation polynomial evaluations
eval_zwuint256Shifted permutation evaluation
Wxi, Wxiwbytes (128 each)Opening proof commitments

G1 points are encoded as 128-byte uncompressed coordinates (64 bytes x, 64 bytes y) per EIP-2537 format.

3.2 Circle STARK Proof Format

When proofSystem is "circle-stark", the proof field is a single bytes blob — the serialized STARK proof as produced by the Stwo prover, verified by the on-chain CircleStarkVerifier.

3.3 Transfer Fields

FieldTypeDescription
proofobject or bytesZK proof (format depends on proofSystem)
nullifierbytes32Nullifier of the spent input note
stateRootuint256Merkle root the proof was generated against
aspRootuint256ASP Merkle root for compliance check
tokenaddressERC-20 token address
outputCommitment1bytes32Commitment of the payment note (for the service)
outputCommitment2bytes32Commitment of the change note (back to agent)
encryptedNote1bytesAES-GCM encrypted payment note
encryptedNote2bytesAES-GCM encrypted change note

3.4 Withdraw Fields

FieldTypeDescription
proofobject or bytesZK proof
nullifierbytes32Nullifier of the payment note created by the transfer
stateRootuint256Merkle root (predicted root after transfer insertion)
aspRootuint256ASP Merkle root
aspIduint256ASP ID used for compliance proof
tokenaddressERC-20 token address
amountuint256Withdrawal amount (must be >= server's required amount)
recipientaddressMust match payTo from Payment-Required
isRagequitbooleanWhether this is a ragequit withdrawal (bypasses ASP)

4. Verification Algorithm (Server)

The server MUST perform the following checks before settling:

4.1 Format Validation

  1. scheme equals "private"
  2. proofSystem is in the server's acceptedProofSystems list
  3. All required fields are present and correctly typed
  4. withdraw.recipient equals the server's payTo address
  5. withdraw.amount >= the server's required amount
  6. withdraw.token equals the server's required asset
  7. transfer.token equals withdraw.token
  8. transfer.nullifier != withdraw.nullifier (different notes)

4.2 Freshness Validation

  1. transfer.stateRoot is a known recent root on the pool contract (isKnownRoot())
  2. transfer.nullifier has not been used (!nullifierUsed())
  3. withdraw.nullifier has not been used (!nullifierUsed())

Root Prediction

The withdraw proof's stateRoot must account for the transfer's output commitments being inserted into the Merkle tree. The client predicts the post-transfer root by computing the insertions locally. If another transaction changes the tree between proof generation and settlement, the pool's ROOT_HISTORY_SIZE provides a window of valid recent roots.

4.3 Settlement

  1. Call pool.transfer(...) with the transfer proof and public inputs
  2. Call pool.withdraw(...) with the withdraw proof and public inputs
  3. Verify both transactions succeed

If either transaction reverts, the payment has failed. The server MUST NOT return 200.


5. Settlement Dispatch

The server dispatches to different contract functions based on proofSystem:

proofSystemTransfer FunctionWithdraw FunctionVerifier
plonkpool.transfer(...)pool.withdraw(...)PlonkVerifierBLS12381 (EIP-2537)
circle-starkpool.transferSTARK(...)pool.withdrawSTARK(...)CircleStarkVerifier

PLONK and Circle STARK notes live in separate Merkle trees within the same pool contract. A PLONK transfer creates notes in the Poseidon/BLS12-381 tree; a STARK transfer creates notes in the Keccak/M31 tree.


6. Atomicity

The transfer and withdraw are two separate on-chain transactions. This creates a failure window: the transfer may succeed but the withdraw may fail (e.g., due to root staleness).

A router contract that executes both operations in a single transaction eliminates this risk:

function settleU402(
    TransferCalldata calldata transferData,
    WithdrawCalldata calldata withdrawData
) external {
    pool.transfer(transferData);
    pool.withdraw(withdrawData);
}

If either call reverts, the entire transaction reverts.

6.2 Alternative: Sequential Settlement

If no router is deployed, the server settles sequentially. If the transfer succeeds but the withdraw fails:

  • The payment note exists in the pool but has not been withdrawn
  • The agent's nullifier is consumed (no double-spend risk)
  • The server should retry the withdraw, or return an error so the agent can reclaim the note

7. Error Handling

ErrorHTTP StatusDescription
INVALID_PROOF_FORMAT400Payment header is malformed or missing fields
UNSUPPORTED_PROOF_SYSTEM400Server doesn't accept the offered proof system
RECIPIENT_MISMATCH400withdraw.recipient doesn't match payTo
INSUFFICIENT_AMOUNT402withdraw.amount is less than required
STALE_ROOT409Merkle root is no longer in the pool's history window
NULLIFIER_USED409One of the nullifiers has already been spent
PROOF_VERIFICATION_FAILED402On-chain proof verification reverted
SETTLEMENT_FAILED502On-chain transaction reverted for other reasons

8. Compliance

u402 inherits UPP's compliance model. The aspId field in both the Payment-Required and withdraw proof determines which ASP's allowlist is checked during proof verification.

  • ASP ID > 0: The withdraw circuit verifies that the original deposit address is in the specified ASP's Merkle tree. The verifier learns "this address is in the approved set" without learning which address.
  • ASP ID = 0 + ragequit: The original depositor can withdraw to their deposit address regardless of ASP status. No compliance proof required.

Servers choose their compliance requirements by setting aspId. Agents that cannot satisfy a particular ASP can use ragequit to reclaim their funds.

For details on running an ASP, see UPC Operators.


9. Reference Implementations

ComponentRepositoryDescription
Pool contractuniversal-private-pooltransfer(), withdraw(), transferSTARK(), withdrawSTARK()
Proof generation@permissionless-technologies/upp-sdksnarkjs.plonk (BLS12-381) and stwo (Circle STARK) provers
ASP compliance@permissionless-technologies/upc-sdkASP client, Merkle tree management, proof generation
Starter templatex402-upd-starterNext.js server + client with x402 and u402 support

On this page