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
- Protocol Specification — full format definitions and verification algorithm
- Security Model — privacy guarantees and threat model
- UPP SDK — proof generation client
- UPC Operators — running an ASP for compliance