u402 — Private Agent Payments

Quickstart

Integrate u402 private payments into your server and client — accept private agent payments or pay privately as an agent.

u402 Quickstart

Pre-release

u402 is under development. The server and client handlers described below track the current UPP SDK and pool contract interfaces. API surface may change.

Prerequisites

  • A deployed UPP pool contract (Sepolia: 0x3b5ed60adbd5dd57f47ec54179e4b1c06636285b)
  • An ERC-20 token shielded in the pool
  • The UPP SDK installed for proof generation

Server: Accepting u402 Payments

1. Return 402 with private scheme

When an unauthenticated request arrives at a paid endpoint, return a 402 response with the payment requirements:

export function createPaymentRequired(config: {
  pool: Address
  asset: Address
  payTo: Address
  amount: bigint
  aspId?: number
  acceptedProofSystems?: string[]
}) {
  return new Response(null, {
    status: 402,
    headers: {
      'Payment-Required': JSON.stringify({
        scheme: 'private',
        network: `eip155:${chainId}`,
        pool: config.pool,
        asset: config.asset,
        payTo: config.payTo,
        amount: config.amount.toString(),
        aspId: config.aspId ?? 1,
        acceptedProofSystems: config.acceptedProofSystems ?? ['plonk'],
      }),
    },
  })
}

2. Verify and settle

When the client retries with a Payment-Signature header:

export async function verifyAndSettle(
  paymentSignature: string,
  requirements: PaymentRequired,
  walletClient: WalletClient,
) {
  const payment = JSON.parse(paymentSignature)

  // Format checks
  if (payment.scheme !== 'private') throw new Error('INVALID_SCHEME')
  if (payment.withdraw.recipient !== requirements.payTo) throw new Error('RECIPIENT_MISMATCH')
  if (BigInt(payment.withdraw.amount) < BigInt(requirements.amount)) throw new Error('INSUFFICIENT_AMOUNT')

  // Settle on-chain
  if (payment.proofSystem === 'plonk') {
    // Submit transfer proof
    await walletClient.writeContract({
      address: requirements.pool,
      abi: poolAbi,
      functionName: 'transfer',
      args: [/* transfer proof fields */],
    })

    // Submit withdraw proof
    await walletClient.writeContract({
      address: requirements.pool,
      abi: poolAbi,
      functionName: 'withdraw',
      args: [/* withdraw proof fields */],
    })
  } else if (payment.proofSystem === 'circle-stark') {
    // Dispatch to STARK functions
    await walletClient.writeContract({
      address: requirements.pool,
      abi: poolAbi,
      functionName: 'transferSTARK',
      args: [/* STARK transfer proof */],
    })
    await walletClient.writeContract({
      address: requirements.pool,
      abi: poolAbi,
      functionName: 'withdrawSTARK',
      args: [/* STARK withdraw proof */],
    })
  }
}

Atomic Settlement

For production use, deploy a router contract that executes both transfer() and withdraw() in a single transaction. See Specification — Atomicity.

Client: Paying with u402

1. Detect 402 and parse requirements

const response = await fetch('https://api.example.com/data')

if (response.status === 402) {
  const requirements = JSON.parse(
    response.headers.get('Payment-Required')!
  )

  if (requirements.scheme === 'private') {
    // Handle u402 payment
    const paymentHeader = await generatePrivatePayment(requirements)

    const paidResponse = await fetch('https://api.example.com/data', {
      headers: { 'Payment-Signature': JSON.stringify(paymentHeader) },
    })
  }
}

2. Generate ZK proofs

The client generates two proofs locally using the UPP SDK:

import { createUPPClient } from '@permissionless-technologies/upp-sdk'

async function generatePrivatePayment(requirements: PaymentRequired) {
  const upp = await createUPPClient({ walletClient })

  // Find a spendable note with sufficient balance
  const notes = await upp.scan()
  const spendableNote = notes.find(
    n => n.token === requirements.asset
      && !n.spent
      && n.amount >= BigInt(requirements.amount)
  )

  // Generate transfer proof: split note → payment + change
  const transferResult = await upp.transfer({
    inputNote: spendableNote,
    amount: BigInt(requirements.amount),
    to: requirements.payTo, // stealth address or direct
  })

  // Generate withdraw proof against predicted post-transfer root
  const withdrawResult = await upp.withdraw({
    inputNote: transferResult.paymentNote,
    amount: BigInt(requirements.amount),
    recipient: requirements.payTo,
    aspId: requirements.aspId,
  })

  return {
    scheme: 'private',
    proofSystem: 'plonk', // or 'circle-stark'
    transfer: transferResult.proofData,
    withdraw: withdrawResult.proofData,
  }
}

Proof Generation Time

PLONK proof generation takes 5–30 seconds in a browser (snarkjs WASM). For latency-sensitive agents, consider pre-generating proofs or maintaining pre-split payment notes.

Starter Template

The x402-upd-starter provides a complete working example with both x402 (plain) and u402 (private) payment flows in a Next.js application.

Next Steps

On this page