https://github.com/forgesworn/toll-booth-rs
L402 payment middleware for Rust. Gates any HTTP API behind Lightning payments.
https://github.com/forgesworn/toll-booth-rs
axum bitcoin http-402 l402 lightning macaroons middleware payments paywall rust
Last synced: 3 months ago
JSON representation
L402 payment middleware for Rust. Gates any HTTP API behind Lightning payments.
- Host: GitHub
- URL: https://github.com/forgesworn/toll-booth-rs
- Owner: forgesworn
- License: mit
- Created: 2026-03-27T17:46:44.000Z (3 months ago)
- Default Branch: main
- Last Pushed: 2026-03-27T21:20:21.000Z (3 months ago)
- Last Synced: 2026-03-28T01:48:32.908Z (3 months ago)
- Topics: axum, bitcoin, http-402, l402, lightning, macaroons, middleware, payments, paywall, rust
- Language: Rust
- Homepage: https://github.com/forgesworn/toll-booth-rs
- Size: 65.4 KB
- Stars: 0
- Watchers: 0
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- License: LICENSE
Awesome Lists containing this project
README
# toll-booth
L402 payment middleware for Rust. Gates any HTTP API behind Lightning payments using a bring-your-own-backend, bring-your-own-storage model. Works as a Tower `Layer`, so it drops straight into axum.
One Lightning payment funds multiple API calls. The engine tracks credit balances, debits per request, and re-challenges when the balance runs out. Supports free tiers, tiered pricing, cost reconciliation, and custom macaroon caveats.
## Why toll-booth?
- **Credit-balance model** -- one payment buys N requests, not one. Clients top up and keep calling.
- **Cost reconciliation** -- adjust credits after the upstream responds if actual cost differed from estimate.
- **Free tier** -- configurable free requests or free credits per IP per day, with hashed IPs and daily rotation.
- **Tiered pricing** -- different prices per route per tier, selected by `X-Toll-Tier` header.
- **Pluggable everything** -- bring your own Lightning backend, storage backend, and payment rails.
- **Security defaults** -- constant-time HMAC and preimage verification, overflow-checked arithmetic, daily-salted IP hashing.
## Quick start
```toml
[dependencies]
toll-booth = { version = "0.1", features = ["l402", "axum-middleware"] }
```
```rust
use std::sync::Arc;
use toll_booth::{
TollBoothEngine, TollBoothConfig,
L402Rail, L402RailConfig,
MemoryStorage,
TollBoothLayer,
PricingEntry,
};
async fn handler() -> &'static str {
"Hello from the paid API"
}
#[tokio::main]
async fn main() {
let storage = Arc::new(MemoryStorage::new());
let l402 = L402Rail::new(L402RailConfig {
root_key: std::env::var("L402_ROOT_KEY").unwrap(),
storage: storage.clone(),
default_amount: 100, // sats, used when no backend is attached
backend: None, // swap in PhoenixdBackend or similar
service_name: Some("My API".to_string()),
});
let mut pricing = std::collections::HashMap::new();
pricing.insert("/v1/chat/completions".to_string(), PricingEntry::Simple(100));
let engine = TollBoothEngine::new(TollBoothConfig {
storage,
pricing,
upstream: "http://127.0.0.1:8080".into(),
root_key: std::env::var("L402_ROOT_KEY").unwrap(),
rails: vec![Box::new(l402)],
..Default::default()
})
.unwrap();
let app = axum::Router::new()
.route("/v1/chat/completions", axum::routing::post(handler))
.layer(TollBoothLayer::new(engine));
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
axum::serve(listener, app).await.unwrap();
}
```
A request to a priced route without credentials gets a `402 Payment Required` with:
- `WWW-Authenticate: L402 macaroon="...", invoice="..."`
- JSON body: `{ "l402": { "macaroon": "...", "payment_hash": "...", "invoice": "...", "amount_sats": 100 } }`
The client pays the Lightning invoice, then retries with:
```
Authorization: L402 :
```
The engine verifies the preimage against the payment hash, credits the account, and debits on each subsequent request until the balance is exhausted.
## Bring Your Own Backend
Implement `LightningBackend` to connect to any Lightning node:
```rust
use async_trait::async_trait;
use toll_booth::{LightningBackend, Invoice, InvoiceStatus, BackendError};
pub struct MyBackend { /* ... */ }
#[async_trait]
impl LightningBackend for MyBackend {
async fn create_invoice(
&self,
amount_sats: u64,
memo: Option<&str>,
) -> Result {
// call your node, return Invoice { bolt11, payment_hash }
todo!()
}
async fn check_invoice(
&self,
payment_hash: &str,
) -> Result {
// return InvoiceStatus { paid, preimage }
todo!()
}
}
```
Pass it to `L402RailConfig::backend`:
```rust
backend: Some(Arc::new(MyBackend::new(/* ... */))),
```
When `backend` is `None`, the engine generates a random `payment_hash` and never checks payment status. Useful for development and testing.
## Bring Your Own Storage
Implement `StorageBackend` to persist credit balances to any store:
```rust
use toll_booth::{StorageBackend, DebitResult, StorageError, StoredInvoice, Currency};
pub struct MyStorage { /* ... */ }
impl StorageBackend for MyStorage {
fn credit(&self, payment_hash: &str, amount: i64, currency: Currency)
-> Result<(), StorageError>;
fn debit(&self, payment_hash: &str, amount: i64, currency: Currency)
-> Result;
fn balance(&self, payment_hash: &str, currency: Currency)
-> Result;
fn settle_with_credit(
&self,
payment_hash: &str,
amount: i64,
settlement_secret: Option<&str>,
currency: Currency,
) -> Result;
fn is_settled(&self, payment_hash: &str) -> Result;
// ... plus invoice store, pruning, and adjust_credits
}
```
`MemoryStorage` is provided for tests and quick prototypes. The `sqlite` feature adds a `SqliteStorage` backend (bundled SQLite, no system dependency).
## Credit model
On first use of a payment credential, the engine calls `settle_with_credit`, which atomically marks the payment settled and credits the `amount_sats` from the macaroon. Subsequent requests debit from that balance. When the balance drops below the route cost, the engine issues a fresh 402.
`TollBoothEngine::reconcile(payment_hash, actual_cost)` adjusts the balance after the upstream responds if the actual cost differed from the estimated cost.
## Free tier
```rust
use toll_booth::FreeTierConfig;
TollBoothConfig {
free_tier: Some(FreeTierConfig::Requests(10)), // 10 free requests per IP per day
// or:
free_tier: Some(FreeTierConfig::Credits(500)), // 500 free sats per IP per day
..Default::default()
}
```
Free-tier checks run after all payment rails are exhausted. IPs are hashed with a daily salt before storage.
## Feature flags
| Feature | Default | Description |
|-------------------|---------|----------------------------------------------------------|
| `l402` | yes | L402 payment rail (macaroon + Lightning preimage) |
| `axum-middleware` | yes | Tower `Layer` for axum (`TollBoothLayer`) |
| `sqlite` | yes | SQLite storage backend (bundled, no system dep) |
| `phoenixd` | no | Phoenixd Lightning node backend |
## Licence
MIT