Rust SDK Documentation

Complete reference for MPP-NEAR Rust SDK. Build payment-gated APIs with type-safe primitives.

Features

  • Stateless verification — HMAC-based challenge binding
  • Type-safe API — Builder patterns with compile-time checks
  • MPP-1.0 compliant — Spec-conformant implementation
  • Axum integration — Middleware & extractors included
  • Extensible — Custom payment methods via traits

Installation

# Cargo.toml
[dependencies]
mpp-near = { git = "https://github.com/kampouse/mpp-near", features = ["server"] }
tokio = { version = "1", features = ["full"] }
axum = "0.7"
serde = { version = "1", features = ["derive"] }
serde_json = "1"

Core Types

Challenge

Payment requirements returned with 402

RequestData

Payment amount and details

Credential

Proof of payment from client

Receipt

Payment confirmation

Problem

RFC 9457 error responses

Verifier

Payment verification trait

Challenge Type

Fields

pub struct Challenge {
    pub id: String,              // Unique identifier (HMAC-derived)
    pub realm: String,           // Protection space (e.g., "api.example.com")
    pub method: String,          // Payment method (e.g., "near-intents")
    pub intent: String,          // Intent: "charge" | "session" | ...
    pub request: String,         // Base64url-encoded RequestData
    pub expires: Option<String>, // RFC 3339 timestamp
    pub digest: Option<String>,  // RFC 9530 body digest
    pub description: Option<String>, // Human-readable description
    pub opaque: Option<String>,  // Server correlation data
}

ChallengeBuilder

use mpp_near::{Challenge, RequestData};

let challenge = Challenge::builder()
    .realm("api.example.com")
    .method("near-intents")
    .intent("charge")
    .request(RequestData::new("0.001", "wallet.near"))
    .currency("USDC")
    .expires(300)           // TTL in seconds (default: 300)
    .secret(b"your-hmac-secret")
    .description("Image generation API")
    .build()?;

Builder Methods

MethodDescription
realm()Set protection space
method()Set payment method
intent()Set payment intent
request()Set RequestData
expires()Set TTL (seconds)
digest()Bind to request body
description()Human-readable description
opaque()Correlation data
secret()HMAC secret for signing

RequestData Type

Fields

pub struct RequestData {
    pub amount: String,                        // Decimal amount
    pub currency: Option<String>,               // Token symbol (e.g., "USDC")
    pub token_id: Option<String>,               // Contract address
    pub recipient: String,                     // Recipient address
    pub chain: Option<String>,                  // Blockchain/network
    pub method_details: Option<Value>,          // Method-specific data
    pub extra: HashMap<String, Value>,         // Additional fields
}

Methods

use mpp_near::RequestData;

// Create with required fields
let request = RequestData::new("0.001", "wallet.near");

// Builder pattern for optional fields
let request = RequestData::new("0.001", "wallet.near")
    .currency("USDC")
    .token_id("usdc.contract.near")
    .chain("near")
    .method_details(json!({"swap": "auto"}))
    .extra("max_slippage", json!("0.5"))
    .encode()?;  // Encode to base64url

Credential Type

Fields

pub struct Credential {
    pub challenge: ChallengeEcho,    // Challenge reference
    pub proof: PaymentProof,         // Method-specific proof
    pub expires: String,             // RFC 3339 timestamp
    pub signature: String,           // Client signature
}

Creating Credentials (Client-side)

use mpp_near::Credential;

let credential = Credential::builder()
    .challenge(&challenge)
    .proof("intent_hash_from_payment")
    .sign(client_private_key)?
    .build()?;

Verifying Credentials (Server-side)

use mpp_near::{Credential, VerificationResult};

// From Authorization header
let credential = Credential::from_authorization(auth_header)?;

// Verify challenge binding
match credential.verify(&challenge) {
    VerificationResult::Valid => {
        // Payment verified successfully
    },
    VerificationResult::Invalid(reason) => {
        // Payment verification failed
        return Err(reason);
    },
}

Receipt Type

Issue receipts to confirm successful payments. Return via Payment-Receipt header.

use mpp_near::Receipt;

let receipt = Receipt::builder()
    .challenge_id(&challenge.id)
    .payer(Some("user.near"))
    .amount("0.001")
    .currency("USDC")
    .intent("charge")
    .build()?;

// Convert to header value
let header_value = receipt.to_header();
response.headers.insert("Payment-Receipt", header_value);

Receipt Fields

FieldTypeDescription
challenge_idStringReference to challenge
payerOption<String>Payer address
amountStringAmount paid
currencyStringToken symbol
intentStringPayment intent
timestampStringISO 8601 timestamp

PaymentProof Type

Payment proof contains method-specific payment data from the credential.

pub struct PaymentProof {
    pub proof: String,                          // Transaction hash
    pub account: Option<String>,                  // Payer account
    pub signature: Option<String>,               // Transaction signature
    pub public_key: Option<String>,              // Payer public key
    pub extra: HashMap<String, Value>,           // Additional data
}

Creating PaymentProof

use mpp_near::PaymentProof;

// Simple proof
let proof = PaymentProof::new("intent_hash_here");

// From credential payload (JSON)
let proof = PaymentProof::from_payload(&credential_json)?;

Body Digest (RFC 9530)

Bind challenges to request bodies to prevent tampering. Clients cannot modify the body after receiving a challenge.

pub struct BodyDigest {
    pub algorithm: DigestAlgorithm,  // Sha256 or Sha512
    pub hash: String,               // Base64-encoded hash
}

pub enum DigestAlgorithm {
    Sha256,  // SHA-256
    Sha512,  // SHA-512
}

Creating Body Digests

use mpp_near::BodyDigest;

// From request body
let body = b"{\"prompt\": \"generate image\"}";
let digest = BodyDigest::sha256(body);

// Format: "sha-256=:base64hash:"
let digest_header = digest.to_header();

// Add to challenge
let challenge = Challenge::builder()
    .request(RequestData::new("0.001", "wallet.near"))
    .body_digest(digest)
    .build()?;

Supported Algorithms

AlgorithmUse Case
sha-256Default, fast
sha-512Higher security

HTTP Headers

Response Headers (402)

HeaderDescription
WWW-AuthenticateChallenge (base64)
Payment-RequiredAlways "402"
Cache-Control"no-store" (prevent caching)

Request Headers (Payment)

HeaderDescription
AuthorizationCredential (base64)
Content-DigestBody digest (if binding)

Response Headers (200)

HeaderDescription
Payment-ReceiptReceipt (base64)

Verification

Verifier Trait

Implement custom verification logic via the Verifier trait:

use mpp_near::Verifier;

pub trait Verifier {
    fn verify(&self, credential: &Credential)
        -> VerificationResult;
}

// Implementation provided for:
// - Challenge binding verification
// - Signature validation
// - Expiry checking

VerificationResult

pub enum VerificationResult {
    Valid,                      // Payment verified
    Invalid(String),            // Verification failed with reason
    Expired,                    // Challenge/expired
}

Custom Payment Methods

Implement custom payment methods via the Method trait. This allows integration with any payment network.

Method Trait

use mpp_near::Method;
use async_trait::async_trait;

#[async_trait]
pub trait Method: Send + Sync {
    /// Method identifier (e.g., "near-intents", "stripe")
    fn id(&self) -> &str;

    /// Build a challenge for this method
    fn build_challenge(
        &self,
        request: &PaymentRequest,
        secret: &[u8]
    ) -> Result<Challenge>;

    /// Verify a payment proof
    async fn verify(
        &self,
        request: &PaymentRequest,
        proof: &PaymentProof,
    ) -> Result<bool>;

    /// Extract request from challenge
    fn extract_request(
        &self,
        challenge: &Challenge
    ) -> Result<PaymentRequest>;

    /// Extract proof from credential
    fn extract_proof(
        &self,
        credential: &Credential
    ) -> Result<PaymentProof>;

    /// Verify full credential
    async fn verify_credential(
        &self,
        challenge: &Challenge,
        credential: &Credential,
    ) -> Result<bool>;
}

PaymentRequest

pub struct PaymentRequest {
    pub amount: String,           // Amount to pay
    pub currency: Option<String>,  // Currency/token
    pub token_id: Option<String>,  // Token contract
    pub recipient: String,         // Recipient address
    pub chain: Option<String>,     // Blockchain/network
    pub challenge_id: String,      // Challenge reference
    pub realm: String,             // Protection space
    pub method: String,            // Payment method
    pub intent: String,            // Payment intent
}

PaymentProof

pub struct PaymentProof {
    pub proof: String,                        // Transaction hash
    pub account: Option<String>,               // Payer account
    pub signature: Option<String>,             // Transaction signature
    pub public_key: Option<String>,            // Payer public key
    pub extra: HashMap<String, Value>,         // Additional fields
}

MethodRegistry

Register and manage multiple payment methods:

use mpp_near::MethodRegistry;

let mut registry = MethodRegistry::new();
registry.register(MyCustomMethod);
registry.register(AnotherMethod);

// Check if method exists
if registry.contains("my-method") {
    let method = registry.get("my-method").unwrap();
    // Use method to verify payment
}

// List all methods
let methods = registry.list();

Registry Methods

MethodDescription
new()Create empty registry
register()Register a method
get()Get method by ID
contains()Check if method exists
list()List all method IDs

Error Handling

Problem Type (RFC 9457)

Return structured error responses with Problem:

use mpp_near::Problem;

// Payment insufficient
let problem = Problem::payment_insufficient(
    "0.01",    // required
    "0.001",   // actual
);

// Verification failed
let problem = Problem::verification_failed(
    "Invalid signature"
);

// Invalid challenge
let problem = Problem::invalid_challenge(
    "Challenge expired"
);

// Returns JSON:
// {
//   "type": "https://mpp.dev/problems/payment-insufficient",
//   "title": "Payment Insufficient",
//   "detail": "Required: 0.01, Actual: 0.001",
//   "status": 402
// }
let json = serde_json::to_string(&problem)?;

Error Types (Problem)

TypeDescription
payment_insufficientAmount too low
verification_failedInvalid proof/signature
invalid_challengeChallenge malformed
challenge_expiredChallenge too old
unsupported_methodUnknown payment method

Error Enum

Full error enum with all variants:

pub enum Error {
    InvalidChallenge(String),      // Invalid challenge format
    InvalidCredential(String),      // Invalid credential format
    ChallengeExpired,               // Challenge expired
    ChallengeNotFound,              // Challenge not found
    VerificationFailed(String),     // Payment verification failed
    UnsupportedMethod(String),      // Unsupported payment method
    InvalidAmount(String),          // Invalid amount
    Http(http::Error),              // HTTP error
    Json(serde_json::Error),        // JSON error
    Base64(base64::DecodeError),    // Base64 decode error
    Other(String),                  // Other error
}

Result Type

Type alias for Result with Error:

use mpp_near::Result;

// Result type alias
pub type Result<T> = std::result::Result<T, Error>;

// Usage
fn create_challenge() -> Result<Challenge> {
    // Returns Result<Challenge, Error>
    Ok(challenge)
}

Constants

MPP-NEAR provides protocol-level constants for versioning and defaults:

/// MPP protocol version
pub const VERSION: &str = "MPP/1.0";

/// Default challenge TTL in seconds
pub const DEFAULT_CHALLENGE_TTL: i64 = 300; // 5 minutes

Usage

use mpp_near::{VERSION, DEFAULT_CHALLENGE_TTL};

// Use in responses
response.headers.insert("MPP-Version", VERSION.parse()?);

// Use for default TTL
let ttl = DEFAULT_CHALLENGE_TTL;  // 300 seconds

// Override as needed
let challenge = Challenge::builder()
    .expires(DEFAULT_CHALLENGE_TTL)
    .build()?;

Note

VERSION is included in all challenges and credentials for protocol negotiation. DEFAULT_CHALLENGE_TTL is used when no explicit TTL is set via .expires().

Complete Example

Payment-Gated API Endpoint

use axum::{Json, http::StatusCode, extract::State};
use mpp_near::{Challenge, Credential, RequestData, Receipt};
use std::sync::Arc;

async fn protected_image_generation(
    State(secret): State<Arc<Vec<u8>>>,
) -> Result<Json<&'static str>, StatusCode> {
    // Create payment challenge
    let challenge = Challenge::builder()
        .realm("api.example.com")
        .method("near-intents")
        .intent("charge")
        .request(
            RequestData::new("0.001", "wallet.near")
                .currency("USDC")
        )
        .description("AI image generation")
        .secret(&secret)
        .build()?;

    // Return 402 with challenge
    Err(StatusCode::PAYMENT_REQUIRED)
        // Include WWW-Authenticate header with challenge
}

Axum Middleware

Pre-built Axum middleware and extractors are available:

// Middleware features:
- Payment verification middleware
- Credential extractor
- Challenge builder helper
- Problem response helper

// Available with:
mpp-near = { git = "https://github.com/kampouse/mpp-near",
            features = ["server"] }