Tenzro Testnet is live. Get testnet TNZO

x402 Payment Protocol

x402 is Coinbase's HTTP 402 payment protocol designed for one-time, per-request payments. Unlike MPP which supports session management and streaming workflows, x402 focuses on simplicity: each API request requires a separate payment. This makes x402 ideal for simple API access, single-shot AI inference requests, and scenarios where session state is undesirable or unnecessary.

Protocol Overview

x402 implements a streamlined two-phase flow:

  1. Payment Required Response: When a client requests a paid resource without payment proof, the server responds with HTTP 402 and an X402PaymentRequired structure specifying the payment amount, recipient, and facilitator information.
  2. Payment Payload Submission: The client creates an X402PaymentPayload proving they have paid (or will pay) the required amount. This payload is attached to the retry request via the X-Payment header. The server verifies the payment and grants access.

x402 is stateless by design: the server does not maintain session state between requests. Each request is fully self-contained with payment proof. This simplifies server implementation but requires clients to pay for every request individually.

Core Components

X402PaymentRequired

The X402PaymentRequired structure is returned by the server in the HTTP 402 response body. It specifies payment requirements:

  • amount: Payment amount in smallest denomination (e.g., 1e18 = 1 TNZO)
  • currency: "TNZO" on Tenzro Network
  • recipient: Payment recipient wallet address (API provider)
  • facilitator: Optional payment facilitator service (Coinbase Pay, etc.)
  • memo: Optional payment memo/reference (invoice ID, resource identifier)
  • expires_at: Payment requirement expiration timestamp
// Server creates payment requirement use tenzro_payments::x402::*; let payment_required = X402PaymentRequired { amount: 100_000_000_000_000_000, // 0.1 TNZO currency: "TNZO".into(), recipient: provider_wallet_address.clone(), facilitator: Some(X402Facilitator { name: "Tenzro Network".into(), url: "https://api.tenzro.network/payment".into(), supported_methods: vec!["wallet".into(), "hosted".into()], }), memo: Some("invoice-12345".into()), expires_at: Some(chrono::Utc::now() + chrono::Duration::minutes(10)), }; // Return HTTP 402 with payment requirement HttpResponse::PaymentRequired() .header("WWW-Authenticate", "x402") .json(&payment_required)

X402PaymentPayload

The X402PaymentPayload is created by the client after receiving a payment requirement. It proves payment was made:

  • payment_id: Unique payment identifier (UUID)
  • payer: Client wallet address
  • recipient: Must match payment requirement recipient
  • amount: Amount paid (must match or exceed requirement)
  • currency: Must match payment requirement currency
  • transaction_hash: On-chain transaction hash proving payment (optional for pre-authorization)
  • signature: Ed25519 signature over canonical payload by payer
  • timestamp: Payment creation timestamp
// Client creates payment payload use tenzro_wallet::WalletService; use tenzro_crypto::signatures::Ed25519Signer; // Option 1: Pay first, then create payload with transaction hash let tx = wallet.send_transaction( payment_required.recipient.clone(), payment_required.amount, Asset::Tnzo, ).await?; let mut payload = X402PaymentPayload { payment_id: Uuid::new_v4().to_string(), payer: wallet.get_address(&Asset::Tnzo).await?, recipient: payment_required.recipient.clone(), amount: payment_required.amount, currency: payment_required.currency.clone(), transaction_hash: Some(tx.hash.clone()), signature: vec![], timestamp: chrono::Utc::now(), }; // Option 2: Create payload as pre-authorization (no tx hash yet) // Server will check signature and may escrow payment let mut payload = X402PaymentPayload { payment_id: Uuid::new_v4().to_string(), payer: wallet.get_address(&Asset::Tnzo).await?, recipient: payment_required.recipient.clone(), amount: payment_required.amount, currency: payment_required.currency.clone(), transaction_hash: None, // Server will pull funds or verify balance signature: vec![], timestamp: chrono::Utc::now(), }; // Sign payload let signing_payload = payload.signing_payload(); let signature = wallet.sign(&signing_payload).await?; payload.signature = signature; // Attach to retry request client .post(original_url) .header("X-Payment", base64::encode(serde_json::to_vec(&payload)?)) .json(&request_body) .send() .await?

X402Facilitator

The optional X402Facilitator structure advertises payment facilitation services. Clients without direct wallet access can redirect to the facilitator URL for hosted payment:

pub struct X402Facilitator { pub name: String, // "Coinbase Pay", "Tenzro Wallet" pub url: String, // Redirect URL for payment flow pub supported_methods: Vec<String>, // ["wallet", "hosted", "card"] } // Client redirects to facilitator for payment if let Some(facilitator) = payment_required.facilitator { let payment_url = format!( "{}?amount={}&currency={}&recipient={}&return_url={}", facilitator.url, payment_required.amount, payment_required.currency, payment_required.recipient, url::encode(original_url), ); // Redirect user to payment_url // After payment, facilitator redirects back with payment payload }

Complete x402 Flow Example

Here is a complete example showing the full x402 lifecycle from payment requirement to verification:

use tenzro_payments::x402::*; use tenzro_payments::PaymentProtocol; use tenzro_wallet::WalletService; // 1. CLIENT: Initial request without payment let response = client .get("https://api.provider.com/data/premium-dataset") .send() .await?; // 2. SERVER: Returns HTTP 402 with payment requirement if response.status() == 402 { let payment_required: X402PaymentRequired = response.json().await?; println!("Payment required: {} {}", payment_required.amount as f64 / 1e18, payment_required.currency); // 3. CLIENT: Execute payment on-chain let tx = wallet.send_transaction( payment_required.recipient.clone(), payment_required.amount, Asset::Tnzo, ).await?; println!("Payment sent: {}", tx.hash); // 4. CLIENT: Create and sign payment payload let mut payload = X402PaymentPayload { payment_id: Uuid::new_v4().to_string(), payer: wallet.get_address(&Asset::Tnzo).await?, recipient: payment_required.recipient.clone(), amount: payment_required.amount, currency: payment_required.currency.clone(), transaction_hash: Some(tx.hash.clone()), signature: vec![], timestamp: chrono::Utc::now(), }; let signing_payload = payload.signing_payload(); let signature = wallet.sign(&signing_payload).await?; payload.signature = signature; // 5. CLIENT: Retry request with payment proof let response = client .get("https://api.provider.com/data/premium-dataset") .header("X-Payment", base64::encode(serde_json::to_vec(&payload)?)) .send() .await?; // 6. SERVER: Verify payment payload let x402 = X402Protocol::new(challenge_store.clone(), settlement_engine.clone()); let payment_header = request.headers().get("X-Payment") .ok_or("Missing payment")?; let payload_bytes = base64::decode(payment_header)?; let payload: X402PaymentPayload = serde_json::from_slice(&payload_bytes)?; // Verify signature let verification = x402.verify_credential(&payload).await?; if !verification.verified { return HttpResponse::PaymentRequired() .json(&json!({ "error": "Invalid payment proof" })); } // Verify on-chain transaction if hash provided if let Some(tx_hash) = &payload.transaction_hash { let tx = blockchain.get_transaction(tx_hash).await?; if tx.to != payment_required.recipient || tx.amount < payment_required.amount { return HttpResponse::PaymentRequired() .json(&json!({ "error": "Invalid transaction" })); } } // 7. SERVER: Return protected resource HttpResponse::Ok() .json(&premium_dataset); // 8. CLIENT: Access granted let data = response.json().await?; Ok(data) }

Server-Side Implementation

Implementing an x402-protected endpoint on Tenzro Network:

use tenzro_payments::x402::*; use axum::{Extension, Json, http::StatusCode}; #[derive(Clone)] pub struct X402Config { pub amount: u64, pub currency: String, pub recipient: String, } async fn protected_endpoint( Extension(x402_config): Extension<X402Config>, Extension(x402_protocol): Extension<Arc<X402Protocol>>, headers: HeaderMap, ) -> Result<Json<DataResponse>, (StatusCode, Json<X402PaymentRequired>)> { // Check for payment header let payment_header = match headers.get("X-Payment") { Some(h) => h, None => { // No payment, return 402 with requirement let payment_required = X402PaymentRequired { amount: x402_config.amount, currency: x402_config.currency.clone(), recipient: x402_config.recipient.clone(), facilitator: Some(X402Facilitator { name: "Tenzro Network".into(), url: "https://wallet.tenzro.network/pay".into(), supported_methods: vec!["wallet".into()], }), memo: Some("data-access".into()), expires_at: Some(chrono::Utc::now() + chrono::Duration::minutes(10)), }; return Err((StatusCode::PAYMENT_REQUIRED, Json(payment_required))); } }; // Decode and verify payment payload let payload_bytes = base64::decode(payment_header.to_str().unwrap()) .map_err(|_| ( StatusCode::BAD_REQUEST, Json(X402PaymentRequired::default()), ))?; let payload: X402PaymentPayload = serde_json::from_slice(&payload_bytes) .map_err(|_| ( StatusCode::BAD_REQUEST, Json(X402PaymentRequired::default()), ))?; // Verify signature and amounts let verification = x402_protocol.verify_credential(&payload).await .map_err(|_| ( StatusCode::PAYMENT_REQUIRED, Json(X402PaymentRequired::default()), ))?; if !verification.verified { return Err(( StatusCode::PAYMENT_REQUIRED, Json(X402PaymentRequired::default()), )); } // Payment verified, return protected resource Ok(Json(DataResponse { data: fetch_premium_data().await, })) }

HTTP Middleware for Automatic x402

Tenzro provides axum middleware for automatic x402 challenge/verification:

use tenzro_payments::middleware::X402Middleware; use axum::{Router, routing::get}; let x402_config = X402Config { amount: 100_000_000_000_000_000, // 0.1 TNZO per request currency: "TNZO".into(), recipient: provider_wallet.get_address(&Asset::Tnzo).await?, }; let x402_middleware = X402Middleware::new( x402_protocol.clone(), x402_config.clone(), ); let app = Router::new() .route("/premium-data", get(handle_premium_data)) .route("/premium-inference", get(handle_premium_inference)) .layer(x402_middleware); // Middleware automatically: // 1. Checks for X-Payment header // 2. If missing, returns HTTP 402 with payment requirement // 3. If present, verifies payment payload signature // 4. Validates transaction hash if provided // 5. Allows request on successful verification

Comparison: x402 vs MPP

When should you use x402 versus MPP on Tenzro Network?

Use x402 when:

  • You want simple, stateless payment verification
  • Each request is independent (no multi-turn workflows)
  • Payment amount is fixed per request
  • You don't need session management overhead
  • Clients can tolerate one on-chain transaction per request
  • You want Coinbase ecosystem compatibility

Use MPP when:

  • You need session management for multi-turn conversations
  • Payment amounts vary per request (e.g., per-token AI inference)
  • You want to amortize on-chain costs across many requests
  • You need prepaid balance and voucher management
  • You want streaming workflows with incremental billing
  • You want Stripe/Tempo ecosystem integration

Integration with Tenzro Settlement

x402 integrates with Tenzro's settlement engine for payment finalization:

use tenzro_settlement::{SettlementEngine, SettlementRequest, SettlementMethod}; // Server settles x402 payment let settlement_request = SettlementRequest { request_id: payload.payment_id.clone(), payer: payload.payer.clone(), recipient: payload.recipient.clone(), amount: payload.amount, currency: Asset::Tnzo, method: SettlementMethod::Immediate, proof: None, // x402 uses signature verification, not ZK proofs }; let settlement_result = settlement_engine .settle(settlement_request) .await?; // If payload included transaction_hash, verify it matches settlement if let Some(expected_tx) = payload.transaction_hash { if settlement_result.transaction_hash != expected_tx { return Err("Transaction hash mismatch")?; } } // Settlement confirmed, grant access log::info!("x402 payment settled: {} TNZO from {} to {}", payload.amount as f64 / 1e18, payload.payer, payload.recipient );

Client SDK Integration

Tenzro's TypeScript SDK provides automatic x402 support:

import { TenzroClient } from '@tenzro/sdk'; const client = new TenzroClient({ rpcUrl: 'https://rpc.tenzro.network', wallet: myWallet, // Connects to browser wallet or private key }); // SDK automatically handles x402 flow try { const data = await client.fetchWithPayment( 'https://api.provider.com/premium-data', { method: 'GET', autoPayX402: true, // Enable automatic x402 payment } ); console.log('Data received:', data); } catch (err) { if (err.code === 'PAYMENT_REQUIRED') { console.log('Payment required:', err.paymentDetails); // Manual payment flow if needed const payment = await client.createX402Payment(err.paymentDetails); const data = await client.fetchWithPayment(url, { payment: payment, }); } } // Under the hood, SDK: // 1. Makes initial request // 2. Receives HTTP 402 with X402PaymentRequired // 3. Creates payment transaction on-chain // 4. Builds X402PaymentPayload with transaction hash // 5. Signs payload with wallet // 6. Retries request with X-Payment header // 7. Returns response data

Security Considerations

Signature Verification: Always verify the payment payload signature using Ed25519 via tenzro_crypto::signatures::verify(). Unsigned payloads must be rejected.

Transaction Validation: If a transaction hash is provided, verify it on-chain before granting access. Check recipient, amount, and confirmation status.

Amount Matching: Verify payment amount matches or exceeds the requirement. Reject underpayments.

Expiration: If the payment requirement includes an expiration, reject stale payment payloads.

Replay Protection: Track used payment IDs to prevent replay attacks. A payment payload should only grant access once.

Next Steps