Rust SDK for the Machine Payments Protocol
MPP (the Machine Payments Protocol) is an open standard for machine-to-machine payments, co-authored by Tempo and Stripe. Paying for a resource over HTTP typically requires API keys, billing accounts, or checkout flows set up ahead of time. MPP lets any client, including an AI agent, an app, or a person, pay as part of the HTTP exchange using the native 402 Payment Required response.
This crate is the Rust SDK for MPP. It supports developers building either side of a paid HTTP exchange: servers that charge for access with Tempo or Stripe, and clients that automatically handle 402 payment challenges. Typical scenarios include gating an API behind per-call payments or building an agent that pays for tools or data as it works. See the quickstart for server and client patterns.
MPP has official SDKs in multiple languages:
| Language | Repository |
|---|---|
| Rust | mpp-rs (this repository) |
| Go | tempoxyz/mpp-go |
| Python | tempoxyz/pympp |
| TypeScript | wevm/mppx |
| Ruby | stripe/mpp-rb |
cargo add mppuse mpp::server::{Mpp, tempo, TempoConfig};
let mpp = Mpp::create(tempo(TempoConfig {
recipient: "0x742d35Cc6634C0532925a3b844Bc9e7595f1B0F2",
}))?;
let challenges = mpp.charge("1")?; // OUSD, then USDC.e on mainnet
let receipt = mpp.verify_charge(&credential, "1").await?;use mpp::server::{Mpp, stripe, StripeConfig};
let mpp = Mpp::create_stripe(stripe(StripeConfig {
secret_key: "sk_test_...",
network_id: "internal",
payment_method_types: &["card"],
currency: "usd",
decimals: 2,
}))?;
let challenge = mpp.stripe_charge("1")?;
let receipt = mpp.stripe_verify_charge(&credential, "1").await?;verify_charge and stripe_verify_charge take the amount the route charges and reject a credential that was issued for a different amount, so a payment for a cheap route cannot be replayed on an expensive one served by the same Mpp. broadcast_credential skips that comparison: it accepts any challenge the Mpp issued, which is only safe when all of them are interchangeable.
If the endpoint already uses Authorization (API keys, Bearer tokens), create the server with requires_auth(true). Challenges then advertise header="Payment-Authorization". The reqwest send_with_payment and PaymentMiddleware APIs reject those challenges before paying: they accept caller-owned clients and cannot enforce a safe redirect policy for the custom credential header. Use a transport that sends credential-bearing requests with redirects disabled until these APIs support a verified redirect-safe client.
let mpp = Mpp::create(tempo(config).requires_auth(true))?;use mpp::client::{PaymentMiddleware, TempoProvider};
use reqwest_middleware::ClientBuilder;
let provider = TempoProvider::new(signer, "https://rpc.moderato.tempo.xyz")?;
let client = ClientBuilder::new(reqwest::Client::new())
.with(PaymentMiddleware::new(provider))
.build();
// Explicitly authorize payment for this request.
let resp = client.get("https://mpp.dev/api/ping/paid")
.header("Accept-Payment", "tempo/charge")
.send().await?;use mpp::client::{Fetch, StripeProvider};
use mpp::protocol::methods::stripe::CreateTokenResult;
let provider = StripeProvider::new(|params| {
Box::pin(async move {
// Proxy SPT creation through your backend (requires Stripe secret key)
let resp = reqwest::Client::new()
.post("https://my-server.com/api/create-spt")
.json(¶ms)
.send().await?.json::<serde_json::Value>().await?;
Ok(CreateTokenResult::from(resp["spt"].as_str().unwrap().to_string()))
})
});
let resp = reqwest::Client::new()
.get("https://api.example.com/paid")
.header("Accept-Payment", "stripe/charge")
.send_with_payment(&provider)
.await?;use mpp::server::ws::{WsMessage, WsResponse};
// Server: parse incoming WS message, send challenge/receipt
let msg: WsMessage = serde_json::from_str(&text)?;
if let WsMessage::Credential { credential } = msg {
let parsed = mpp::parse_authorization(&credential)?;
let receipt = mpp.verify_charge(&parsed, "0.01").await?; // the amount this socket charges
let resp = WsResponse::Receipt {
receipt: serde_json::to_value(&receipt)?,
};
socket.send(resp.to_text()).await;
}
// Client: detect challenge, send credential
let msg: mpp::client::ws::WsServerMessage = serde_json::from_str(&text)?;
if let WsServerMessage::Challenge { challenge, .. } = msg {
let cred_msg = serde_json::json!({
"type": "credential",
"credential": auth_string,
});
ws.send(cred_msg.to_string()).await;
}WSS (WebSocket Secure) is handled at the connection layer. The transport itself is protocol-agnostic. On the server, terminate TLS via a reverse proxy (nginx, Cloudflare) or use axum-server with rustls. On the client, tokio-tungstenite supports wss:// URLs via its native-tls or rustls features:
tokio-tungstenite = { version = "0.26", features = ["rustls-tls-webpki-roots"] }| Feature | Description |
|---|---|
client |
Client-side payment providers (PaymentProvider trait, Fetch extension) |
server |
Server-side payment verification (ChargeMethod trait) |
tempo |
Tempo blockchain support (includes evm) |
stripe |
Stripe payment support via SPTs |
evm |
Shared EVM utilities (Address, U256, parsing) |
middleware |
reqwest-middleware support with PaymentMiddleware (implies client) |
tower |
Tower middleware for server-side integration |
axum |
Axum extractor support for server-side convenience |
ws |
WebSocket transport for bidirectional session payments |
utils |
Hex/random utilities for development and testing |
MPP supports multiple payment methods through one protocol: Tempo, Stripe, Lightning, Card, and custom methods. The server advertises which methods it accepts, and the client chooses which one to pay with. This SDK implements Tempo (charge and session intents) and Stripe (charge intent via Shared Payment Tokens).
Built on the "Payment" HTTP Authentication Scheme. The source specifications are maintained in tempoxyz/mpp-specs. See mpp.dev/protocol for the protocol overview or paymentauth.org for the wire format.
git clone https://github.com/tempoxyz/mpp-rs
cd mpp-rs
cargo test
See SECURITY.md for reporting vulnerabilities.
Licensed under either of Apache License, Version 2.0 or MIT License at your option.
Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in these crates by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.
HTTP payment helpers now default to AcceptPaymentPolicy::Explicit: set a request-level Accept-Payment header to authorize automatic payment. Reqwest client default headers cannot be inspected by middleware and do not authorize payments. A request-level q=0 remains an opt-out. Disabled prevents all automatic payment, even with an explicit header. Applications that intentionally authorize provider-selected payment can pass Always; this explicitly overrides opaque client defaults. Origin policies authorize automatic payment only for matching origins unless a request supplies its own preference.
ChannelStoreAdapter requires a native atomic Store::compare_exchange. Implement it as a transaction or conditional write shared by every client using the backend; a read followed by a write is unsafe. MemoryStore supports it. FileStore and custom backends without this operation fail closed on channel updates. Concurrent conflicts fail before resource delivery and must be retried against fresh state.
An unconfirmed channel open must be reconciled through a valid receipt or the session recovery flow before increasing its voucher authorization. Cancellation preserves the channel descriptor and durable balance for recovery; it does not erase potentially funded state.
Pending session opens retain their original authorized cumulative amount across
retries and Rust SQLite-store restarts. A retry can resubmit that amount; it cannot
increase it until a matching payment receipt or validated recovery snapshot is
accepted. For native sessions, bootstrap can reconcile a matching server
snapshot. Streaming voucher increases and top-ups are refused while an opening
is unresolved. Retain the channel descriptor and use close to recover a funded
channel if the server cannot reconcile it. Do not share an unresolved channel
with older SDK versions that do not preserve the pending-open marker.