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:
- Payment Required Response: When a client requests a paid resource without payment proof, the server responds with HTTP 402 and an
X402PaymentRequiredstructure specifying the payment amount, recipient, and facilitator information. - Payment Payload Submission: The client creates an
X402PaymentPayloadproving they have paid (or will pay) the required amount. This payload is attached to the retry request via theX-Paymentheader. 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 movesamountfrompayertorecipient. 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 oftenzro_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={}¤cy={}&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 verificationComparison: 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 dataSecurity 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
- Explore Tempo integration for stablecoin settlement
- Review settlement architecture for payment finalization
- Compare with MPP for session-based payments
- Understand TDIP identity binding for delegated authorization