@thecryptodonkey/toll-booth
v3.4.0
Published
Monetise any API with HTTP 402 payments. Payment-rail agnostic middleware for Express, Hono, Deno, Bun, and Workers.
Maintainers
Readme
toll-booth
Monetise any API with one line of code.

Live demo - pay 21 sats, get a joke. No account. No sign-up. (API)
Try it now
npx @thecryptodonkey/toll-booth demoSpins up a fully working L402-gated joke API on localhost. Mock Lightning backend, in-memory storage, zero configuration. Scan the QR code from your terminal when the free tier runs out.
Minimal example
import express from 'express'
import { Booth } from '@thecryptodonkey/toll-booth'
import { phoenixdBackend } from '@thecryptodonkey/toll-booth/backends/phoenixd'
const app = express()
const booth = new Booth({
adapter: 'express',
backend: phoenixdBackend({ url: 'http://localhost:9740', password: process.env.PHOENIXD_PASSWORD! }),
pricing: { '/api': 10 }, // 10 sats per request
upstream: 'http://localhost:8080', // your existing API
})
app.get('/invoice-status/:paymentHash', booth.invoiceStatusHandler as express.RequestHandler)
app.post('/create-invoice', booth.createInvoiceHandler as express.RequestHandler)
app.use('/', booth.middleware as express.RequestHandler)
app.listen(3000)The old way vs toll-booth
| | The old way | With toll-booth |
|---|---|---|
| Step 1 | Create a Stripe account | npm install @thecryptodonkey/toll-booth |
| Step 2 | Verify your identity (KYC) | Set your pricing: { '/api': 10 } |
| Step 3 | Integrate billing SDK | app.use(booth.middleware) |
| Step 4 | Build a sign-up page | Done. No sign-up page needed. |
| Step 5 | Handle webhooks, refunds, chargebacks | Done. Payments are final. |
Five zeroes
Zero accounts. Zero API keys. Zero chargebacks. Zero KYC. Zero vendor lock-in.
Your API earns money the moment it receives a request. Clients pay with Lightning, Cashu ecash, or NWC — no relationship with you required. Payments settle instantly and are cryptographically final — no disputes, no reversals, no Stripe risk reviews.
See it in production
satgate is a pay-per-token AI inference proxy built on toll-booth. It monetises any OpenAI-compatible endpoint — Ollama, vLLM, llama.cpp — with one command. Token counting, model pricing, streaming reconciliation, capacity management. Everything else — payments, credits, free tier, macaroon auth — is toll-booth.
~400 lines of product logic on top of the middleware. That's what "monetise any API with one line of code" looks like in practice.
Let AI agents pay for your API
toll-booth is the server side of a two-part stack for machine-to-machine payments. 402-mcp is the client side - an MCP server that gives AI agents the ability to discover, pay, and consume L402-gated APIs autonomously.
AI Agent -> 402-mcp -> toll-booth -> Your APIAn agent using Claude, GPT, or any MCP-capable model can call your API, receive a 402 payment challenge, pay the Lightning invoice from its wallet, and retry - all without human intervention. No OAuth dance, no API key rotation, no billing portal.
Live demo
Visit jokes.trotters.dev in a browser to try it - get a free joke, hit the paywall, scan the QR code or pay with a browser wallet extension.
Or use the API directly:
# Get a free joke (1 free per day per IP)
curl https://jokes.trotters.dev/api/joke
# Free tier exhausted - request a Lightning invoice for 21 sats
curl -X POST https://jokes.trotters.dev/create-invoice
# Pay the invoice with any Lightning wallet, then authenticate
curl -H "Authorization: L402 <macaroon>:<preimage>" https://jokes.trotters.dev/api/jokeFeatures
- L402 protocol - industry-standard HTTP 402 payment flow with macaroon credentials
- Multiple Lightning backends - Phoenixd, LND, CLN, LNbits, NWC (any Nostr Wallet Connect wallet)
- Alternative payment methods - Cashu ecash tokens
- Cashu-only mode - no Lightning node required; ideal for serverless and edge deployments
- Credit system - pre-paid balance with volume discount tiers
- Free tier - configurable daily allowance (IP-hashed, no PII stored)
- Privacy by design - no personal data collected or stored; IP addresses are one-way hashed with a daily-rotating salt before any processing
- Self-service payment page - QR codes, tier selector, wallet adapter buttons
- SQLite persistence - WAL mode, automatic invoice expiry pruning
- Three framework adapters - Express, Web Standard (Deno/Bun/Workers), and Hono
- Framework-agnostic core - use the
Boothfacade or wire handlers directly
Quick start
npm install @thecryptodonkey/toll-boothExpress
import express from 'express'
import { Booth } from '@thecryptodonkey/toll-booth'
import { phoenixdBackend } from '@thecryptodonkey/toll-booth/backends/phoenixd'
const app = express()
app.use(express.json())
const booth = new Booth({
adapter: 'express',
backend: phoenixdBackend({
url: 'http://localhost:9740',
password: process.env.PHOENIXD_PASSWORD!,
}),
pricing: { '/api': 10 }, // 10 sats per request
upstream: 'http://localhost:8080', // your API
rootKey: process.env.ROOT_KEY, // 64 hex chars, required for production
})
app.get('/invoice-status/:paymentHash', booth.invoiceStatusHandler as express.RequestHandler)
app.post('/create-invoice', booth.createInvoiceHandler as express.RequestHandler)
app.use('/', booth.middleware as express.RequestHandler)
app.listen(3000)Web Standard (Deno / Bun / Workers)
import { Booth } from '@thecryptodonkey/toll-booth'
import { lndBackend } from '@thecryptodonkey/toll-booth/backends/lnd'
const booth = new Booth({
adapter: 'web-standard',
backend: lndBackend({
url: 'https://localhost:8080',
macaroon: process.env.LND_MACAROON!,
}),
pricing: { '/api': 5 },
upstream: 'http://localhost:8080',
})
// Deno example
Deno.serve({ port: 3000 }, async (req: Request) => {
const url = new URL(req.url)
if (url.pathname.startsWith('/invoice-status/'))
return booth.invoiceStatusHandler(req)
if (url.pathname === '/create-invoice' && req.method === 'POST')
return booth.createInvoiceHandler(req)
return booth.middleware(req)
})Hono
import { Hono } from 'hono'
import { createHonoTollBooth, type TollBoothEnv } from '@thecryptodonkey/toll-booth/hono'
import { phoenixdBackend } from '@thecryptodonkey/toll-booth/backends/phoenixd'
import { createTollBooth } from '@thecryptodonkey/toll-booth'
import { sqliteStorage } from '@thecryptodonkey/toll-booth/storage/sqlite'
const storage = sqliteStorage({ path: './toll-booth.db' })
const engine = createTollBooth({
backend: phoenixdBackend({ url: 'http://localhost:9740', password: process.env.PHOENIXD_PASSWORD! }),
storage,
pricing: { '/api': 10 },
upstream: 'http://localhost:8080',
rootKey: process.env.ROOT_KEY!,
})
const tollBooth = createHonoTollBooth({ engine })
const app = new Hono<TollBoothEnv>()
// Mount payment routes
app.route('/', tollBooth.createPaymentApp({
storage,
rootKey: process.env.ROOT_KEY!,
tiers: [],
defaultAmount: 1000,
}))
// Gate your API
app.use('/api/*', tollBooth.authMiddleware)
app.get('/api/resource', (c) => {
const balance = c.get('tollBoothCreditBalance')
return c.json({ message: 'Paid content', balance })
})
export default appCashu-only (no Lightning node)
import { Booth } from '@thecryptodonkey/toll-booth'
const booth = new Booth({
adapter: 'web-standard',
redeemCashu: async (token, paymentHash) => {
// Verify and redeem the ecash token with your Cashu mint
// Return the amount redeemed in satoshis
return amountRedeemed
},
pricing: { '/api': 5 },
upstream: 'http://localhost:8080',
})No Lightning node, no channels, no liquidity management. Ideal for serverless and edge deployments.
Lightning backends
import { phoenixdBackend } from '@thecryptodonkey/toll-booth/backends/phoenixd'
import { lndBackend } from '@thecryptodonkey/toll-booth/backends/lnd'
import { clnBackend } from '@thecryptodonkey/toll-booth/backends/cln'
import { lnbitsBackend } from '@thecryptodonkey/toll-booth/backends/lnbits'
import { nwcBackend } from '@thecryptodonkey/toll-booth/backends/nwc'Each backend implements the LightningBackend interface (createInvoice + checkInvoice).
| Backend | Status | Notes | |---------|--------|-------| | Phoenixd | Stable | Simplest self-hosted option | | LND | Stable | Industry standard | | CLN | Stable | Core Lightning REST API | | LNbits | Stable | Any LNbits instance - self-hosted or hosted | | NWC | Stable | Any Nostr Wallet Connect wallet (Alby Hub, Mutiny, Umbrel, Phoenix, etc.) — E2E encrypted via NIP-44 |
Why not Aperture?
Aperture is Lightning Labs' production L402 reverse proxy. It's battle-tested and feature-rich. Use it if you can.
| | Aperture | toll-booth | |---|---|---| | Language | Go binary | TypeScript middleware | | Deployment | Standalone reverse proxy | Embeds in your app, or runs as a gateway in front of any HTTP service | | Lightning node | Requires LND | Phoenixd, LND, CLN, LNbits, or none (Cashu-only) | | Serverless | No - long-running process | Yes - Web Standard adapter runs on Cloudflare Workers, Deno, Bun | | Configuration | YAML file | Programmatic (code) |
What about x402?
x402 is Coinbase's HTTP 402 payment protocol for on-chain stablecoins. Great — the more protocols normalising HTTP 402 as a payment primitive, the better for everyone.
toll-booth is payment-rail agnostic. It already supports Lightning (five backends), Cashu ecash, and Nostr Wallet Connect. x402 is on the roadmap as another pluggable backend. When it lands, a single toll-booth deployment will accept Lightning and x402 stablecoins and Cashu — simultaneously. The seller doesn't care how they get paid. They just want paid.
The unique value of toll-booth isn't any single payment rail. It's the middleware layer: gating, credit accounting, free tiers, volume discounts, upstream proxying, and macaroon credentials — all framework-agnostic, all runtime-agnostic, all payment-rail agnostic. Payment protocols are pluggable backends. toll-booth is the booth.
Using toll-booth with any API
toll-booth works as a reverse proxy gateway, so the upstream API can be written in any language - C#, Go, Python, Ruby, Java, or anything else that speaks HTTP. The upstream service doesn't need to know about L402 or Lightning; it just receives normal requests.
Client ---> toll-booth (Node.js) ---> Your API (any language)
| |
L402 payment gating Plain HTTP requests
Macaroon verification X-Credit-Balance header addedPoint upstream at your existing service:
const booth = new Booth({
adapter: 'express',
backend: phoenixdBackend({ url: '...', password: '...' }),
pricing: { '/api/search': 5, '/api/generate': 20 },
upstream: 'http://my-dotnet-api:5000', // ASP.NET, FastAPI, Gin, Rails...
})Deploy toll-booth as a sidecar (Docker Compose, Kubernetes) or as a standalone gateway in front of multiple services. See examples/valhalla-proxy/ for a complete Docker Compose reference - the Valhalla routing engine it gates is a C++ service.
Production checklist
- Set a persistent
rootKey(64 hex chars / 32 bytes). Without it, a random key is generated per restart and all existing macaroons become invalid. Generate one with:node -e "console.log(require('crypto').randomBytes(32).toString('hex'))" - Use a persistent
dbPath(default:./toll-booth.db). - Enable
strictPricing: trueto prevent unpriced routes from bypassing billing. - Ensure your
pricingkeys match the paths the middleware actually sees (after mounting). - Set
trustProxy: truewhen behind a reverse proxy, or provide agetClientIpcallback for per-client free-tier isolation. - If you implement
redeemCashu, make it idempotent for the samepaymentHash- crash recovery depends on it. - Rate-limit
/create-invoiceat your reverse proxy - each call creates a real Lightning invoice.
Example deployments
sats-for-laughs - build your own paid API
examples/sats-for-laughs/ is the fastest path from "I have an API" to "my API earns sats". It's the same code that runs the live demo. Includes a web frontend with QR codes and wallet adapter buttons, plus a JSON API for programmatic access. Clone it, change three env vars, deploy.
cd examples/sats-for-laughs
cp .env.example .env # add your Phoenixd credentials
docker compose up -d # or: MOCK=true npm startIncludes mock mode for local development (auto-settles invoices, no Lightning node needed), Docker Compose with Phoenixd, and a pre-generated pool of 100+ jokes across six topics.
valhalla-proxy - production reference
examples/valhalla-proxy/ gates the Valhalla routing engine (a C++ service) behind Lightning payments. Full Docker Compose setup demonstrating toll-booth as a sidecar proxy in front of non-JavaScript infrastructure.
Payment flow
sequenceDiagram
participant C as Client
participant T as toll-booth
participant U as Upstream API
C->>T: GET /api/resource
T->>T: Check free tier
alt Free tier available
T->>U: Proxy request
U-->>T: Response
T-->>C: 200 + X-Free-Remaining header
else Free tier exhausted
T-->>C: 402 + Invoice + Macaroon
C->>C: Pay via Lightning / Cashu / NWC
C->>T: GET /api/resource<br/>Authorization: L402 macaroon:preimage
T->>T: Verify macaroon + preimage
T->>T: Settle credit + debit cost
T->>U: Proxy request
U-->>T: Response
T-->>C: 200 + X-Credit-Balance header
end- Client requests a priced endpoint without credentials
- Free tier checked - if allowance remains, request passes through
- If exhausted - 402 response with BOLT-11 invoice + macaroon
- Client pays via Lightning, NWC, or Cashu
- Client sends
Authorization: L402 <macaroon>:<preimage> - Macaroon verified, credit deducted, request proxied upstream
Configuration
The five most common options:
| Option | Type | Description |
|--------|------|-------------|
| adapter | 'express' \| 'web-standard' \| 'hono' | Framework integration to use |
| backend | LightningBackend | Lightning node (optional if using Cashu-only) |
| pricing | Record<string, number> | Route pattern to cost in sats |
| upstream | string | URL to proxy authorised requests to |
| freeTier | { requestsPerDay: number } or { creditsPerDay: number } | Daily free allowance per IP (request-count or sats-budget) |
See docs/configuration.md for the full reference including rootKey, creditTiers, trustProxy, nwcPayInvoice, redeemCashu, and all other options.
Documentation
| Document | Description | |----------|-------------| | Why L402? | The case for permissionless, machine-to-machine payments | | Architecture | How toll-booth, satgate, and 402-mcp fit together | | Configuration | Full reference for all Booth options | | Deployment | Docker, nginx, Cloudflare Workers, Deno, Bun, Hono | | Security | Threat model, macaroon security, hardening measures | | Migration | Upgrading from v1 to v2, and v2 to v3 | | Contributing | Development setup, conventions, adding backends |
Ecosystem
| Project | Role | |---------|------| | toll-booth | Payment-rail agnostic HTTP 402 middleware | | satgate | Production showcase — pay-per-token AI inference proxy (~400 lines on toll-booth) | | 402-mcp | Client side — AI agents discover, pay, and consume L402 APIs |
Support
If you find toll-booth useful, consider sending a tip:
- Lightning:
[email protected] - Nostr zaps:
npub1mgvlrnf5hm9yf0n5mf9nqmvarhvxkc6remu5ec3vf8r0txqkuk7su0e7q2
