bsv-payment-actix-middleware 0.1.0

BSV payment middleware for Actix-web, wire-compatible with the TypeScript payment-express-middleware
Documentation

bsv-payment-actix-middleware

BSV payment middleware for Actix-web, implementing the BRC-103 mutual authentication framework payment layer. Wire-compatible with the TypeScript payment-express-middleware, enabling Rust and Node.js servers to enforce micropayments using the same protocol.

Part of the BSV mutual authentication stack defined by BRC-103, BRC-104, and BRC-29.

Table of Contents

Background

The BSV mutual authentication framework (BRC-103) defines a layered approach to server-to-client communication:

  1. Authentication layer -- verifies the caller's identity using signed request headers and key derivation (BRC-29).
  2. Payment layer -- enforces micropayment requirements on protected routes, returning 402 Payment Required with a nonce when no payment is attached, and verifying/internalizing transactions when a payment header is present.

This crate implements the payment layer as Actix-web middleware. It runs after the authentication middleware and uses the authenticated caller's identity key for key derivation during transaction internalization.

Features

  • 402 Payment Required flows -- automatic challenge/response when a request lacks the X-BSV-Payment header.
  • Configurable pricing -- set a default satoshi amount or provide an async callback that prices each request individually.
  • Nonce-based security -- derivation prefix nonces are created and verified via the wallet's HMAC operations, preventing replay attacks.
  • Auth middleware integration -- reads the authenticated identity from request extensions (or via the optional bridge feature).
  • Automatic transaction internalization -- parses the payment header, decodes the base64 transaction, and calls wallet.internalize_action().
  • Zero-price passthrough -- requests priced at 0 satoshis skip payment enforcement entirely.
  • Panic safety -- all wallet calls are wrapped in catch_unwind to guard against panics in the underlying BSV SDK.
  • Wire compatibility -- JSON error shapes, header names, and camelCase field names match the TypeScript implementation exactly.

Installation

Add the crate to your project:

cargo add bsv-payment-actix-middleware

To enable the auth-to-payment bridge (recommended when using bsv-auth-actix-middleware in the same application):

cargo add bsv-payment-actix-middleware --features bridge

Pre-requisites

  1. Auth middleware -- the payment middleware must run after an authentication middleware that inserts an AuthIdentity into request extensions. Use bsv-auth-actix-middleware with the bridge feature, or insert AuthIdentity manually.

  2. BSV wallet -- a type implementing bsv::wallet::interfaces::WalletInterface from the bsv-sdk crate. The wallet is used for nonce HMAC operations and transaction internalization.

  3. Client with 402 support -- the calling client must understand 402 responses, read the x-bsv-payment-derivation-prefix nonce header, create a payment transaction, and resend the request with the X-BSV-Payment header.

Quick Start

use actix_web::{web, App, HttpServer, HttpResponse};
use bsv_payment_actix_middleware::{
    PaymentMiddlewareConfigBuilder, PaymentMiddlewareFactory, PaymentInfo,
};

// Your wallet type implementing WalletInterface
// use my_app::MyWallet;

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    // let wallet = MyWallet::new().await;

    // let config = PaymentMiddlewareConfigBuilder::new()
    //     .wallet(wallet)
    //     .build()
    //     .expect("valid config");

    HttpServer::new(move || {
        App::new()
            // Payment middleware wraps all routes in this app.
            // In Actix-web, middleware runs in reverse registration order,
            // so register payment BEFORE auth so that auth runs first.
            // .wrap(PaymentMiddlewareFactory::new(config.clone()))
            // .wrap(AuthToPaymentBridge)       // bridge feature
            // .wrap(AuthMiddleware::new(...))   // auth middleware
            .route("/api/paid", web::get().to(paid_handler))
    })
    .bind("127.0.0.1:8080")?
    .run()
    .await
}

async fn paid_handler(payment: PaymentInfo) -> HttpResponse {
    HttpResponse::Ok().json(serde_json::json!({
        "message": "Payment received",
        "satoshis_paid": payment.satoshis_paid,
        "accepted": payment.accepted,
    }))
}

Detailed Usage

Configuration with the Builder

PaymentMiddlewareConfigBuilder provides a fluent API for constructing the middleware configuration:

use bsv_payment_actix_middleware::{
    PaymentMiddlewareConfigBuilder, CalculateRequestPrice,
};
use std::sync::Arc;

let config = PaymentMiddlewareConfigBuilder::new()
    .wallet(my_wallet)
    .calculate_request_price(Box::new(|req| {
        Box::pin(async move {
            // Price based on the request path
            let path = req.path();
            if path.starts_with("/api/premium") {
                Ok(1000u64)
            } else {
                Ok(100u64)
            }
        })
    }))
    .build()
    .expect("valid config");

Builder methods:

Method Description
wallet(W) Required. Set the wallet instance.
calculate_request_price(cb) Optional async callback returning the price in satoshis.
build() Consume the builder and return Result<Config, Error>.

When no calculate_request_price callback is set, the middleware defaults to DEFAULT_SATOSHIS (100).

Middleware Registration

Register the middleware with App::wrap() or Scope::wrap():

use actix_web::{web, App};
use bsv_payment_actix_middleware::PaymentMiddlewareFactory;

App::new()
    .wrap(PaymentMiddlewareFactory::new(config))
    .route("/api/resource", web::get().to(handler));

Actix-web processes middleware in reverse registration order. Place PaymentMiddlewareFactory before your auth middleware in the wrap chain so that auth runs first at request time.

Custom Pricing with CalculateRequestPrice

The CalculateRequestPrice type alias is a boxed async closure:

type CalculateRequestPrice = Box<
    dyn Fn(&ServiceRequest) -> BoxFuture<'static, Result<u64, Box<dyn Error + Send + Sync>>>
    + Send + Sync
>;

Return Ok(0) to allow a request through without payment. Return an error to trigger a 500 response with ERR_PAYMENT_INTERNAL.

Extracting Payment Info in Handlers

After the middleware processes a payment, it inserts PaymentInfo into the request extensions. Extract it in your handler using Actix-web's FromRequest:

use bsv_payment_actix_middleware::PaymentInfo;

async fn handler(payment: PaymentInfo) -> HttpResponse {
    println!("Paid {} satoshis", payment.satoshis_paid);
    println!("Accepted: {:?}", payment.accepted);
    HttpResponse::Ok().finish()
}

Payment Flow (Detailed)

  1. The middleware reads AuthIdentity from request extensions.
  2. It calls the pricing callback (or uses DEFAULT_SATOSHIS).
  3. If the price is 0, the request passes through with a zero-value PaymentInfo.
  4. If no X-BSV-Payment header is present, the middleware:
    • Creates a nonce via create_nonce().
    • Returns a 402 Payment Required response with headers:
      • x-bsv-payment-version -- protocol version
      • x-bsv-payment-satoshis-required -- price
      • x-bsv-payment-derivation-prefix -- nonce for key derivation
  5. If the X-BSV-Payment header is present, the middleware:
    • Parses the JSON header into a BSVPayment struct.
    • Verifies the nonce via verify_nonce().
    • Decodes the base64 transaction.
    • Calls wallet.internalize_action() to accept the payment.
    • Inserts PaymentInfo into request extensions.
    • Adds x-bsv-payment-satoshis-paid to the response headers.
    • Forwards the request to the inner handler.

API Reference

See API.md for the complete public API reference documenting all types, traits, and constants.

Online documentation is available at docs.rs/bsv-payment-actix-middleware.

Example Payment Flows

Zero-Price Passthrough

When the pricing callback returns 0, the middleware inserts a zero-value PaymentInfo and forwards the request immediately. No 402 challenge is issued.

Client                          Server
  |                                |
  |  GET /api/free                 |
  |------------------------------->|
  |                                |  price = 0, skip payment
  |  200 OK                        |
  |<-------------------------------|

Paid Request (402 Challenge then Success)

Client                          Server
  |                                |
  |  GET /api/paid                 |
  |------------------------------->|
  |                                |  No X-BSV-Payment header
  |  402 Payment Required          |
  |  x-bsv-payment-version: 1.0   |
  |  x-bsv-payment-satoshis-required: 100
  |  x-bsv-payment-derivation-prefix: <nonce>
  |<-------------------------------|
  |                                |
  |  (Client creates payment tx)   |
  |                                |
  |  GET /api/paid                 |
  |  X-BSV-Payment: {"derivationPrefix":"<nonce>",
  |    "derivationSuffix":"<suffix>",
  |    "transaction":"<base64-tx>"}|
  |------------------------------->|
  |                                |  Verify nonce
  |                                |  Internalize transaction
  |  200 OK                        |
  |  x-bsv-payment-satoshis-paid: 100
  |<-------------------------------|

Security Considerations

  • Run after auth middleware. The payment middleware reads the caller's identity key from request extensions. Without a preceding auth middleware, requests will receive a 500 ERR_SERVER_MISCONFIGURED error.

  • Nonce handling. Derivation prefix nonces are created and verified using the wallet's HMAC operations. Each nonce is single-use. Replaying an old nonce will result in a 400 ERR_INVALID_DERIVATION_PREFIX error.

  • Error handling. All wallet calls are wrapped in catch_unwind to prevent panics in the BSV SDK from crashing the server. Panics are logged and converted to appropriate error responses.

  • Transaction acceptance. The accepted field in PaymentInfo reflects the wallet's internalization result. Handlers should check this field if they need to verify the payment was fully accepted before serving premium content.

Resources and References

License

MIT