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:
- Challenge: The server responds with HTTP 402 Payment Required, including a challenge specifying the payment amount, currency (TNZO for Tenzro), and acceptance criteria.
- 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. - 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
- Explore x402 for Coinbase's alternative HTTP 402 protocol
- Learn about Tempo integration for stablecoin settlement
- Review settlement architecture for payment finalization
- Understand TDIP identity binding for delegated payment authorization