Tenzro Testnet is live —request 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.

Scheme Adapters

Tenzro's x402 facilitator supports multiple settlement schemes via a pluggable SchemeRegistry. The client picks a scheme by name in the payment payload; the server dispatches to the matching adapter for verification and settlement.

  • exact: Direct on-chain transfer of the exact challenge amount. The payer signs an Ed25519 payload referencing a transaction hash that moves amount from payer to recipient. Simplest, most auditable; one tx per request.
  • permit2: EIP-2612 permit-based authorization. The payer signs an off-chain permit allowing the facilitator to pull funds at settlement time, so the on-chain transfer happens server-side as part of tenzro_payX402. Lets clients pay without first executing their own transaction.

Discover the live scheme list with tenzro_listX402Schemes (or tenzro x402 list-schemes); pay via tenzro_payX402 (or tenzro x402 pay --scheme <name>). New scheme adapters drop in by registering an implementation against SchemeRegistry in tenzro-payments.

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://{provider}.tenzro.network/v1/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://{provider}.tenzro.network/v1/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';
import { highlight } from "sugar-high";

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://{provider}.tenzro.network/v1/data/premium-dataset',
    {
      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