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://{provider}.tenzro.network/v1/chat/completions".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(), "gemma4-9b".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://{provider}.tenzro.network/v1/chat/completions")
    .header("X-MPP-Session-Id", session.session_id)
    .json(&InferenceRequest {
        model: "gemma4-9b".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://{provider}.tenzro.network/v1/chat/completions")
    .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