Skip to content
LogoLogo

Server

Protect endpoints with payment requirements

Create an Mpp instance with Mpp::create() and call charge() with a human-readable dollar amount. The tempo() factory configures recipient once, then every charge() call uses those defaults.

Quick start

use mpp::server::{Mpp, tempo, TempoConfig};
 
let mpp = Mpp::create(tempo(TempoConfig {
    recipient: "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266",
}))?;
 
let challenge = mpp.charge("0.10")?;
let receipt = mpp.verify_credential(&credential).await?;

Mpp::create() auto-detects realm from environment variables (VERCEL_URL, FLY_APP_NAME, HOSTNAME, and others) and reads MPP_SECRET_KEY for stateless HMAC verification. Treat MPP_SECRET_KEY as root-of-trust material: store it in your secret manager, keep it server-side, and rotate it immediately if it is exposed. See Security. Pass explicit values with the builder to override:

let mpp = Mpp::create(
    tempo(TempoConfig {
        recipient: "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266",
    })
    .realm("api.example.com")
    .secret_key("my-server-secret")
)?;

Axum handler example

Use charge() to generate a Challenge and verify_credential() to verify the retry:

use mpp::server::{Mpp, tempo, TempoConfig};
use mpp::{parse_authorization, format_www_authenticate};
 
let mpp = Mpp::create(tempo(TempoConfig {
    recipient: "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266",
}))?;
 
// In your handler, check for an Authorization header:
let auth = headers.get("authorization").and_then(|v| v.to_str().ok());
 
match auth {
    Some(auth_header) => {
        // Parse and verify the Credential
        let credential = parse_authorization(auth_header)?;
        let receipt = mpp.verify_credential(&credential).await?;
        // → return 200 with paid content + Payment-Receipt header
    }
    None => {
        // No Credential — issue a Challenge
        let challenge = mpp.charge("0.50")?;
        let header = format_www_authenticate(&challenge)?;
        // → return 402 with WWW-Authenticate header
    }
}

For a declarative approach with less boilerplate, use the Axum extractor instead.

tempo() builder

tempo() creates a TempoBuilder with smart defaults. Only recipient is required.

use mpp::server::{tempo, TempoConfig};
 
let builder = tempo(TempoConfig {
    recipient: "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266",
})
.currency("0x20c0000000000000000000000000000000000000")
.decimals(6)
.fee_payer(true)
.realm("api.example.com")
.rpc_url("https://rpc.moderato.tempo.xyz")
.secret_key("my-secret");

tempo() parameters

chain_id (optional)

  • Type: u64

Explicitly set the chain ID. Auto-detected from the RPC URL if omitted (moderato → 42431, otherwise → 4217).

currency (optional)

  • Type: &str
  • Default: USDC.e on mainnet, pathUSD on testnet

TIP-20 token address for charges.

decimals (optional)

  • Type: u32
  • Default: 6

Token decimal places for dollar-to-base-unit conversion.

fee_payer (optional)

  • Type: bool
  • Default: false

Enable fee sponsorship for all Challenges. When enabled, the server co-signs and sponsors transaction gas fees.

realm (optional)

  • Type: &str

Server realm for WWW-Authenticate headers. Auto-detected from MPP_REALM, VERCEL_URL, FLY_APP_NAME, HOSTNAME, and others.

rpc_url (optional)

  • Type: &str
  • Default: "https://rpc.tempo.xyz"

Tempo RPC endpoint URL. Also auto-detects chain ID from the URL.

secret_key (optional)

  • Type: &str

HMAC secret for stateless Challenge ID verification. Reads MPP_SECRET_KEY if omitted. Keep it server-side, never log it, and rotate it with an overlap window during rollovers. See Security.

charge() parameters

amount

  • Type: &str

Payment amount in dollars (for example, "0.50" for $0.50). Automatically converted to base units using the configured decimals.

charge_with_options()

Pass ChargeOptions for additional control:

use mpp::server::ChargeOptions;
 
let challenge = mpp.charge_with_options("1.00", ChargeOptions {
    description: Some("Premium content"),
    external_id: Some("order-123"),
    fee_payer: true,
    ..Default::default()
})?;

description (optional)

  • Type: Option<&str>

Human-readable description attached to the Challenge.

expires (optional)

  • Type: Option<&str>

Challenge expiration as ISO 8601 timestamp. Defaults to 5 minutes from now.

external_id (optional)

  • Type: Option<&str>

Merchant reference ID for reconciliation.

fee_payer (optional)

  • Type: bool

Override the server-level fee sponsorship setting for this Challenge.

Verify a Credential

verify_credential decodes the charge request from the echoed challenge automatically—no need to reconstruct the request:

let receipt = mpp.verify_credential(&credential).await?;
println!("Reference: {}", receipt.reference);

To prevent cross-route replay attacks, verify against expected values:

use mpp::ChargeRequest;
 
let expected = ChargeRequest {
    amount: "100000".into(),
    currency: "0x20c0000000000000000000000000000000000000".into(),
    recipient: Some("0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266".into()),
    ..Default::default()
};
 
let receipt = mpp
    .verify_credential_with_expected_request(&credential, &expected)
    .await?;

Axum extractor

The MppCharge<C> extractor handles the full 402 challenge/verify flow automatically. Requires the axum feature.

Define a ChargeConfig type for each price point:

use mpp::server::axum::{ChargeConfig, MppCharge, ChargeChallenger};
use mpp::server::{Mpp, tempo, TempoConfig};
use axum::{routing::get, Router, Json};
use std::sync::Arc;
 
struct OneCent;
impl ChargeConfig for OneCent {
    fn amount() -> &'static str { "0.01" }
}
 
struct OneDollar;
impl ChargeConfig for OneDollar {
    fn amount() -> &'static str { "1.00" }
    fn description() -> Option<&'static str> { Some("Premium content") }
}
 
async fn cheap(charge: MppCharge<OneCent>) -> Json<serde_json::Value> {
    Json(serde_json::json!({ "paid": true, "ref": charge.receipt.reference }))
}
 
async fn expensive(charge: MppCharge<OneDollar>) -> &'static str {
    "premium content"
}
 
let mpp = Mpp::create(tempo(TempoConfig {
    recipient: "0xabc...",
})).unwrap();
 
let app = Router::new()
    .route("/basic", get(cheap))
    .route("/premium", get(expensive))
    .with_state(Arc::new(mpp) as Arc<dyn ChargeChallenger>);

The extractor returns 402 with a WWW-Authenticate Challenge when no Authorization header is present, and extracts a verified Receipt when a valid Credential is provided.

Session support

For payment session channels, add a SessionMethod and generate session Challenges:

use mpp::server::SessionChallengeOptions;
 
let challenge = mpp.session_challenge_with_details(
    "1000",                                             // amount per unit (base units)
    "0x20c0000000000000000000000000000000000000",        // currency
    "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266",        // recipient
    SessionChallengeOptions {
        unit_type: Some("token"),
        suggested_deposit: Some("60000"),
        fee_payer: true,
        ..Default::default()
    },
)?;

Verify session credentials (vouchers):

let result = mpp.verify_session(&credential).await?;
println!("Receipt: {:?}", result.receipt);
 
// Management responses (channel open/close) return a body to forward
if let Some(body) = result.management_response {
    return Ok(Json(body));
}

SessionChallengeOptions parameters

ParameterTypeDescription
descriptionOption<&str>Human-readable description
expiresOption<&str>Challenge expiration (ISO 8601)
fee_payerboolEnable fee sponsorship
suggested_depositOption<&str>Suggested deposit in base units
unit_typeOption<&str>Unit label (for example, "token", "byte")

Advanced API

For full control, use Mpp::new() with a manual TempoChargeMethod:

use mpp::server::{Mpp, tempo_provider, TempoChargeMethod};
 
let provider = tempo_provider("https://rpc.tempo.xyz")?;
let method = TempoChargeMethod::new(provider);
let payment = Mpp::new(method, "api.example.com", "my-server-secret");
 
// Generate challenges with explicit base units
let challenge = payment.charge_challenge(
    "1000000",
    "0x20c0000000000000000000000000000000000000",
    "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266",
)?;
 
// Verify with an explicit request
let receipt = payment.verify(&credential, &charge_request).await?;

Key types

TypeDescription
ChargeMethodTrait for custom charge verification
ChargeOptionsOptions for charge_with_options()
MppServer handler binding method, realm, and secret
SessionChallengeOptionsOptions for session_challenge_with_details()
SessionMethodTrait for session/channel verification
SessionVerifyResultResult of session verification with optional management response
TempoChargeMethodBuilt-in Tempo charge verification
TempoConfigConfiguration struct for the tempo() factory