Two lines of nginx, no API rewrite.
Two lines in your existing nginx config wrap any upstream — no SDK to import, no application code to touch, no language lock-in. The diff fits in one commit, and reviews like any other reverse-proxy change.
ngx-l402 is an open-source nginx module that puts a Lightning paywall in front of any upstream — no API rewrite, no SDK. It speaks L402 (bLIP-26), settles over the Lightning Network you already run, and verifies every payment locally — you keep custody of the sats.
# Put L402 in front of any upstream — no API rewrite. server { listen 443 ssl; server_name api.example.com; location /v1/ { l402 on; l402_amount_msat_default 1000000; # 1,000 sats / request l402_auto_detect_payment on; # client needn't echo the preimage proxy_pass http://upstream; } } # Backend — LND · LNC · CLN · Eclair · LNURL · NWC · BOLT12 — is set via env vars.
↑ that block turns api.example.com/v1/ into a 1,000-sat-per-request Lightning endpoint. No application code touched.
L402 pairs two things: a macaroon (the access token) and a Lightning preimage (proof of payment), tied together by sha256(preimage) == paymentHash. The client — a human, a script, an AI agent — hits the endpoint, pays the returned invoice on Lightning, and retries with the macaroon and preimage. No accounts, no API keys, no chain confirmations.
# Hit the protected endpoint with no auth. $ curl -i https://api.example.com/v1/chat HTTP/1.1 402 Payment Required WWW-Authenticate: L402 macaroon="AGIAJEemVQUTEyNCR0exk7ek...", invoice="lnbc10u1p3xnhl2pp5..." # The whole challenge lives in the WWW-Authenticate header: # a macaroon (access token) + a BOLT-11 invoice to pay.
# Agent pays the invoice with any Lightning wallet. # lncli (LND) shown here — Phoenix, Alby, NWC all work. $ lncli payinvoice \ --pay_req=lnbc10u1p3xnhl2pp5... Payment hash: 9c3a2b1f8e7d6c5b4a39281706f5e4d3... Payment preimage: 7a3c91b4e2d058... Status: SUCCEEDED · Fee: 0 sat · Hops: 3 # sha256(preimage) == paymentHash → proof of payment. # Verified locally at the edge — no callback to any third party.
# Repeat the request with macaroon + preimage. $ curl -H "Authorization: L402 \ $MACAROON:$PREIMAGE" \ https://api.example.com/v1/chat HTTP/1.1 200 OK Content-Type: application/json { "reply": "hello, agent.", "model": "gpt-4o-mini", "tokens": 42 }
Run the real 402 → pay → unlock flow right here — the widget hits the live demo gateway by default, or point it at any ngx-l402 gateway you run.
L402 is interactive: challenge, pay, retry. Cashu is bearer money — the client attaches an ecash token to the request itself and gets the resource in a single round trip. Tokens are blind-signed by the mint, so the payment can't be linked back to the payer.
# No invoice round-trip. Attach a bearer token via X-Cashu. $ curl -H "X-Cashu: cashuApGFtdWh0dHBzOi8vbWlu..." \ https://api.example.com/v1/chat HTTP/1.1 200 OK Content-Type: application/json # The gateway verified the token — offline in P2PK mode (NUT-12 DLEQ), # or via a mint swap in standard mode — and proxied upstream.
| L402 · Lightning invoice | Cashu · ecash token | |
|---|---|---|
| Round trips | 2 — challenge → pay → retry | 1 — the token rides the request |
| Client holds | a Lightning wallet | bearer tokens from a mint |
| Proof of payment | macaroon + preimage | the token itself (optionally P2PK-locked to your gateway) |
| Privacy | invoice visible to the paying node | blind-signed — payment can't be linked to the payer |
| Gateway verification | local hash check | offline DLEQ check (P2PK) or mint swap (standard) |
| Replay protection | spent preimages in Redis | spent tokens in Redis |
// enable: CASHU_ECASH_SUPPORT=true · whitelist mints in production · P2PK mode for high traffic
Two lines in your existing nginx config wrap any upstream — no SDK to import, no application code to touch, no language lock-in. The diff fits in one commit, and reviews like any other reverse-proxy change.
Point ngx-l402 at the Lightning infrastructure you already run. Swap backends without touching your application — change one environment variable and restart nginx.
Payments settle straight to the Lightning node or address you control — no middleman, no platform cut, no third party holding funds or metering your traffic. Every preimage is verified locally at the edge, so nothing about a request leaves your box.
Dynamic pricing — change any route's price with one Redis SET; picked up on the next request, no nginx reload. docs
Dry-run (shadow) mode — evaluate the paywall on live production traffic, log what would happen, block nothing. docs
Prometheus metrics — an l402_metrics scrape endpoint with counters aggregated across all nginx workers. docs
Multi-tenant payouts — per-route LNURL addresses (static or via Redis), so each tenant is paid to their own wallet. docs
Pay once, keep access — l402_indefinite_access turns one payment into a subscription-style credential. docs
Expiring access — l402_macaroon_timeout time-boxes credentials for metered plans. docs
Auto-detect payment — the gateway confirms the invoice settled on its own Lightning node and unlocks automatically, so the client never has to return a preimage — even wallets that can't produce a usable one just work. Available on LND · CLN · BOLT12 · Eclair. docs
gRPC upstreams — the same paywall works in front of grpc_pass services over HTTP/2. docs
ngx-l402 publishes a /.well-known/l402-services manifest — the agent-era equivalent of robots.txt. A client given only a hostname can discover which routes are paid, what they cost, and which payment backends are accepted, then pay and proceed — no API keys, no out-of-band integration, no human in the loop.
{
"version": "1",
"service": { "name": "Example API", "description": "Stock data, paid per request." },
"payment_methods": [
{ "type": "lightning", "backend": "LNURL", "address": "api@getalby.com" },
{ "type": "cashu", "mints": ["https://mint.minibits.cash"], "p2pk_supported": true }
],
"routes": [
{ "path": "/v1/chat", "price": { "type": "static", "amount_msat": 1000000 } }
]
}
↑ one directive — l402_manifest; — makes the whole API surface discoverable: routes, prices, and payment backends, in one JSON document.
Pre-built container, one docker command. The simplest setup needs only an LNURL address to receive at — no node to run. Swap in LND, LNC, CLN, NWC, BOLT12, or Eclair by changing env vars.
$ docker run -d --name l402-nginx -p 8000:8000 \ -e LN_CLIENT_TYPE=LNURL \ -e LNURL_ADDRESS=you@your-lnurl-server.com \ -e ROOT_KEY=$(openssl rand -hex 32) \ ghcr.io/dhananjaypurohit/ngx_l402:latest # Gateway listens on :8000. # curl http://localhost:8000/ → 200 OK # curl -i http://localhost:8000/protected → 402 Payment Required
The image ships nginx, the module, and clients for all backends, with a default /protected route. Mount your own nginx.conf to define paid locations.
Any request to a protected route comes back as 402 Payment Required with a Lightning invoice in the WWW-Authenticate header.
Pay the invoice from any Lightning wallet. Retry with the preimage in the Authorization header — the gateway verifies locally and proxies through to your upstream.
L402 has a healthy ecosystem — but most tools live at a different layer: hosted platforms, or client libraries that pay endpoints. ngx-l402 is the self-hosted, non-custodial server side, as a module in the nginx you already run.
| ngx-l402 | Aperture | Fewsats | Lightning Enable | |
|---|---|---|---|---|
| What it is | nginx module | standalone proxy | hosted platform | hosted layer |
| Self-hosted | yes — your nginx | yes (+ etcd) | no | no (BYO key) |
| Non-custodial | yes | yes | platform-mediated | custodial provider |
| Backends | LND · LNC · CLN · Eclair · LNURL · NWC · BOLT12 · Cashu | LND | abstracted | Strike / OpenNode |
| Node required | no (LNURL / NWC) | yes (LND) | no | no |
| Cashu / privacy | yes | no | no | no |
| Discovery manifest | /.well-known/l402-services | no | index | no |
// Alby, l402-requests and the Fewsats SDK are L402 clients — they pay ngx-l402 endpoints, so they complement it rather than compete.
// snapshot as of July 2026 — these projects evolve. Spot something stale or unfair?
open an issue.
Lightning payments settle straight to your node or address — the module only verifies proof of payment, sha256(preimage) == paymentHash, locally at the edge. Cashu tokens can auto-redeem to Lightning on an interval you set.
Macaroons are bound to the exact request path, and settled preimages and Cashu tokens are recorded in Redis. If Redis is configured but unreachable, the gateway fails closed — it returns 402 rather than letting a request through unpaid.
Per-route, per-IP invoice rate limiting (l402_invoice_rate_limit) caps challenge generation. Cashu P2PK proofs verify offline (NUT-12 DLEQ) — no mint round-trip on the hot path.
No. The LNURL and NWC backends let you point at any Lightning address or wallet — no node to operate. Run LND / CLN / Eclair only if you want to.
Non-custodial. Payments go directly to the node or address you configure. ngx-l402 never holds funds — it only checks proof of payment.
The client simply doesn't get access — it keeps receiving 402. No settlement, no access, and nothing to refund, because the gateway never takes custody.
Set l402_invoice_rate_limit per route to cap invoice generation per IP. With Redis configured, replay and spend-tracking are shared across all nginx workers.
Enable l402_manifest to publish /.well-known/l402-services — a JSON document of routes, prices, and accepted backends. An agent with only your hostname can discover and pay.
Any wallet that returns a standard 32-byte preimage. Note: some wallets (e.g. Wallet of Satoshi) return a non-standard 48-byte preimage that won't work with the classic flow — use auto-detect mode (supported on the LND, CLN, BOLT12, and Eclair backends) or a standard wallet.
nginx 1.28.0 or later. Pre-built .so binaries ship for 1.28.0, 1.28.3, 1.29.8, 1.30.3, and 1.31.2; any other version builds in one command: docker build --build-arg NGX_VERSION=<version> .
Yes — point it at a regtest LND or a Cashu mint for local testing. Mainnet and regtest use the same config.