Tenzro Testnet is live. Get testnet TNZO

Machine Payments Protocol (MPP)

MPP is the Machine Payments Protocol co-authored by Stripe and Tempo Labs. It provides a standardized HTTP 402-based payment flow designed for streaming AI inference, API access, and other machine-to-machine payment scenarios. On Tenzro Network, MPP enables AI agents and models to request payment before serving inference requests, with session management for multi-turn conversations.

Protocol Overview

MPP implements a three-phase flow for metered access to paid resources:

  1. Challenge: The server responds with HTTP 402 Payment Required, including a challenge specifying the payment amount, currency (TNZO for Tenzro), and acceptance criteria.
  2. Credential: The client creates a payment credential proving payment authorization, signed by their wallet. This credential is attached to subsequent requests via the Authorization header.
  3. Receipt: After successful credential verification and settlement, the server returns a signed receipt confirming payment. The client can access the protected resource.

MPP is particularly well-suited for streaming payments where costs accrue over time (e.g., per-token AI inference billing). Tenzro extends MPP with session management via MppSession and MppSessionManager to support multi-request workflows with session vouchers and balance tracking.

Core Components

MppChallenge

The MppChallenge is created by the server (model provider or API endpoint) when an unpaid request arrives. It specifies:

  • challenge_id: Unique identifier for this challenge (UUID)
  • resource_url: The URL of the protected resource
  • amount: Payment amount required (in smallest denomination)
  • currency: "TNZO" on Tenzro Network
  • expires_at: Challenge expiration timestamp (ISO 8601)
  • recipient: Payment recipient address (model provider wallet)
  • metadata: Optional key-value pairs (model ID, rate card, session info)
// Server creates challenge use tenzro_payments::mpp::*; let challenge = MppChallenge { challenge_id: Uuid::new_v4().to_string(), resource_url: "https://api.provider.com/inference".into(), amount: 500_000_000_000_000_000, // 0.5 TNZO (18 decimals) currency: "TNZO".into(), expires_at: chrono::Utc::now() + chrono::Duration::minutes(5), recipient: provider_wallet_address.clone(), metadata: Some(HashMap::from([ ("model_id".into(), "llama-3-70b".into()), ("per_token_rate".into(), "1000000000000000".into()), // 0.001 TNZO/token ])), }; // Return HTTP 402 with challenge in body // WWW-Authenticate: MPP challenge="{base64_encoded_challenge}" HttpResponse::PaymentRequired() .json(&challenge)

MppCredential

The MppCredential is created by the client after receiving a challenge. It proves payment authorization with a cryptographic signature:

  • credential_id: Unique credential identifier
  • challenge_id: Links back to the original challenge
  • payer: Client wallet address
  • recipient: Must match challenge recipient
  • amount: Must match or exceed challenge amount
  • currency: Must match challenge currency
  • signature: Ed25519 signature over canonical credential payload
  • timestamp: Credential creation time
// Client creates credential from challenge use tenzro_wallet::WalletService; use tenzro_crypto::signatures::Ed25519Signer; let credential = MppCredential { credential_id: Uuid::new_v4().to_string(), challenge_id: challenge.challenge_id.clone(), payer: wallet.get_address(&Asset::Tnzo).await?, recipient: challenge.recipient.clone(), amount: challenge.amount, currency: challenge.currency.clone(), signature: vec![], // Will be filled below timestamp: chrono::Utc::now(), }; // Sign credential payload let payload = credential.signing_payload(); let signature = wallet.sign(&payload).await?; credential.signature = signature; // Attach to request via Authorization header // Authorization: MPP credential="{base64_encoded_credential}" client .post(&challenge.resource_url) .header("Authorization", format!("MPP credential={}", base64::encode(&credential))) .send() .await?

MppReceipt

After credential verification and settlement, the server returns an MppReceipt:

  • receipt_id: Unique receipt identifier
  • credential_id: Links back to the credential
  • settlement_tx: On-chain transaction hash proving settlement
  • amount: Amount actually settled
  • currency: Settlement currency
  • timestamp: Receipt creation time
  • server_signature: Server signature over receipt data
// Server creates receipt after successful settlement let receipt = MppReceipt { receipt_id: Uuid::new_v4().to_string(), credential_id: credential.credential_id.clone(), settlement_tx: Some(settlement_result.transaction_hash.clone()), amount: settlement_result.amount, currency: "TNZO".into(), timestamp: chrono::Utc::now(), server_signature: vec![], }; // Sign receipt let payload = receipt.signing_payload(); let signature = server_wallet.sign(&payload).await?; receipt.server_signature = signature; // Return with HTTP 200 response HttpResponse::Ok() .header("X-Payment-Receipt", base64::encode(&receipt)) .json(&inference_result)

Session Management

Tenzro extends MPP with session management for multi-turn conversations and streaming workflows. Instead of creating a new challenge for every request, clients can establish a session with prepaid balance.

MppSession

An MppSession tracks prepaid balance and usage across multiple requests:

pub struct MppSession { pub session_id: String, // UUID pub client: String, // Client wallet address pub provider: String, // Provider wallet address pub balance: u64, // Remaining balance in smallest denomination pub currency: String, // "TNZO" pub created_at: DateTime<Utc>, pub expires_at: DateTime<Utc>, // Session expiration pub vouchers: Vec<SessionVoucher>, // Prepayment vouchers pub usage: Vec<UsageRecord>, // Per-request usage log } pub struct SessionVoucher { pub voucher_id: String, pub amount: u64, pub settlement_tx: String, // On-chain proof of prepayment pub timestamp: DateTime<Utc>, }

Creating a Session

// Client creates session with initial deposit use tenzro_payments::mpp::{MppSessionManager, SessionVoucher}; let session_mgr = MppSessionManager::new(); // First, settle the prepayment on-chain let prepayment_amount = 10_000_000_000_000_000_000; // 10 TNZO let settlement_tx = wallet.send_transaction( provider_address.clone(), prepayment_amount, Asset::Tnzo, ).await?; // Create voucher proving prepayment let voucher = SessionVoucher { voucher_id: Uuid::new_v4().to_string(), amount: prepayment_amount, settlement_tx: settlement_tx.hash.clone(), timestamp: chrono::Utc::now(), }; // Request session creation let session = session_mgr.create_session( client_address.clone(), provider_address.clone(), "TNZO".into(), vec![voucher], chrono::Duration::hours(24), // Session valid for 24 hours ).await?; println!("Session created: {}", session.session_id); println!("Initial balance: {} TNZO", session.balance as f64 / 1e18);

Using a Session

// Client makes requests with session ID instead of per-request payment client .post("https://api.provider.com/inference") .header("X-MPP-Session-Id", session.session_id) .json(&InferenceRequest { model: "llama-3-70b".into(), messages: vec![ Message { role: "user".into(), content: "Hello!".into() }, ], }) .send() .await?; // Server debits session balance after each request session_mgr.debit_session( &session.session_id, request_cost, // Calculated based on tokens used UsageRecord { record_id: Uuid::new_v4().to_string(), request_id: inference_result.request_id.clone(), amount: request_cost, tokens_used: inference_result.usage.total_tokens, timestamp: chrono::Utc::now(), }, ).await?; // Client can query session balance let session = session_mgr.get_session(&session_id).await?; println!("Remaining balance: {} TNZO", session.balance as f64 / 1e18); // Client can top up session with additional vouchers session_mgr.add_voucher(&session_id, new_voucher).await?;

Complete MPP Flow Example

Here is a complete example showing the full MPP lifecycle from challenge to receipt:

use tenzro_payments::mpp::*; use tenzro_payments::{PaymentProtocol, PaymentGateway}; use tenzro_settlement::SettlementEngine; use tenzro_wallet::WalletService; // 1. CLIENT: Initial request without payment let response = client .post("https://api.provider.com/inference") .json(&InferenceRequest { /* ... */ }) .send() .await?; // 2. SERVER: Returns HTTP 402 with challenge if response.status() == 402 { let challenge: MppChallenge = response.json().await?; // 3. CLIENT: Create and sign credential let mut credential = MppCredential { credential_id: Uuid::new_v4().to_string(), challenge_id: challenge.challenge_id.clone(), payer: client_wallet.get_address(&Asset::Tnzo).await?, recipient: challenge.recipient.clone(), amount: challenge.amount, currency: challenge.currency.clone(), signature: vec![], timestamp: chrono::Utc::now(), }; let payload = credential.signing_payload(); let signature = client_wallet.sign(&payload).await?; credential.signature = signature; // 4. CLIENT: Retry request with credential let response = client .post(&challenge.resource_url) .header("Authorization", format!("MPP credential={}", base64::encode(serde_json::to_vec(&credential)?))) .json(&InferenceRequest { /* ... */ }) .send() .await?; // 5. SERVER: Verify credential let mpp = MppProtocol::new(challenge_store.clone(), settlement_engine.clone()); let verification = mpp.verify_credential(&credential).await?; if !verification.verified { return Err("Invalid credential")?; } // 6. SERVER: Settle payment let receipt = mpp.settle(&credential).await?; // 7. SERVER: Return receipt with inference result HttpResponse::Ok() .header("X-Payment-Receipt", base64::encode(serde_json::to_vec(&receipt)?)) .json(&inference_result); // 8. CLIENT: Extract and verify receipt if let Some(receipt_header) = response.headers().get("X-Payment-Receipt") { let receipt_bytes = base64::decode(receipt_header)?; let receipt: MppReceipt = serde_json::from_slice(&receipt_bytes)?; // Verify receipt signature let payload = receipt.signing_payload(); let verified = verify_signature( &payload, &receipt.server_signature, &provider_public_key, )?; if verified { println!("Payment settled: {} TNZO", receipt.amount as f64 / 1e18); println!("Settlement TX: {}", receipt.settlement_tx.unwrap_or_default()); } } let result: InferenceResponse = response.json().await?; Ok(result) }

Integration with Tenzro Identity

MPP payments on Tenzro can be bound to TDIP identities with delegation scope enforcement. This ensures machine identities only make payments within their authorized limits:

use tenzro_payments::identity_binding::*; use tenzro_identity::{IdentityRegistry, DelegationScope}; // Bind payment to machine identity let identity_binding = PaymentIdentityBinding::new(identity_registry.clone()); // Validate payer identity and delegation scope let validation = identity_binding.validate_payer( &payer_did, // did:tenzro:machine:controller:uuid challenge.amount, PaymentProtocolId::Mpp, Some("tenzro-mainnet".into()), ).await?; if !validation.is_valid { return Err(format!("Payment not authorized: {}", validation.reason))?; } // Delegation scope enforcement ensures: // - amount <= max_transaction_value // - daily_spend + amount <= max_daily_spend // - PaymentProtocolId::Mpp in allowed_payment_protocols // - "tenzro-mainnet" in allowed_chains // - current_time within time_bound (if set)

HTTP Middleware for Automatic MPP

Tenzro provides axum middleware for automatic MPP challenge/verification:

use tenzro_payments::middleware::MppMiddleware; use axum::{Router, routing::post}; let mpp_middleware = MppMiddleware::new( mpp_protocol.clone(), "/inference".into(), // Protected path 500_000_000_000_000_000, // 0.5 TNZO per request ); let app = Router::new() .route("/inference", post(handle_inference)) .layer(mpp_middleware); // Middleware automatically: // 1. Checks for Authorization: MPP credential=... header // 2. If missing, returns HTTP 402 with challenge // 3. If present, verifies credential // 4. If valid, allows request and settles payment // 5. Adds X-Payment-Receipt header to response

Security Considerations

Challenge Expiration: Always check challenge expiration before accepting credentials. Tenzro's MPP implementation enforces a 5-minute default expiration.

Signature Verification: Credential signatures are verified using Ed25519 via tenzro_crypto::signatures::verify(). Forged signatures are rejected.

Amount Matching: Credential amount must match or exceed challenge amount. The server can accept overpayment for tipping.

Settlement Atomicity: Settlement failures trigger credential rejection. Receipts are only issued after on-chain settlement confirms.

Session Security: Session vouchers should be verified against on-chain settlement transactions before crediting balance. Voucher signatures prevent balance manipulation.

Next Steps