One nginx directive, 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. Speaks L402 (bLIP-26) today and MPP-Lightning (IETF draft-ryan-httpauth-payment) next, both verifying the same sha256(preimage) primitive on the same gateway.
# Put L402 in front of any upstream — no API rewrite. server { listen 443 ssl; server_name api.example.com; location /v1/ { l402 on; l402_price 1000; # sats per request l402_backend lnd; l402_lnd_host https://lnd.internal:8080; l402_macaroon /etc/lnd/invoice.macaroon; proxy_pass http://upstream; } }
↑ that block turns api.example.com/v1/ into a 1,000-sat-per-request Lightning endpoint. No application code touched.
The same primitive — sha256(preimage) == paymentHash — drives both L402 and MPP-Lightning. The client (a human, a script, an AI agent) hits the endpoint, pays the returned invoice on Lightning, and retries with the 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..." Content-Type: application/json Cache-Control: no-store { "amount_sat": 1000, "description": "chat.completions · 1 request", "expires_at": "2026-05-21T18:04:11Z" }
# Agent pays the invoice on its Lightning node. # Any of 8 backends works — LND shown here. $ lncli payinvoice \ --pay_req=lnbc10u1p3xnhl2pp5... Payment hash: 9c3a2b1f8e7d6c5b4a39281706f5e4d3... Payment preimage: 7a3c91b4e2d058... Status: SUCCEEDED · Fee: 0 sat · Hops: 3 # sha256(preimage) == paymentHash → proof of payment. # Same primitive serves L402 and MPP-Lightning.
# 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 X-L402-Settled: "1000 sat" { "reply": "hello, agent.", "model": "gpt-4o-mini", "tokens": 42 }
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 directive and reload nginx.
Speaks L402 (Lightning Labs bLIP-26) today. The upcoming release adds MPP-Lightning, the Lightning method of the IETF Machine Payments Protocol — both verify the same primitive, so the same Lightning backend serves both.
Pre-built container, one docker command, no dependencies beyond a Lightning node you can reach. (For local testing, point it at a regtest LND or a Cashu mint.)
$ docker run --rm -p 8080:80 \ -v $(pwd)/nginx.conf:/etc/nginx/conf.d/api.conf \ -v $(pwd)/invoice.macaroon:/etc/lnd/invoice.macaroon:ro \ ghcr.io/ngx-l402/ngx-l402:1.2.5 # Gateway listens on :8080. Point your client at it. # Mount your own nginx.conf + Lightning credentials.
Drop a nginx.conf with one l402-enabled location into the container. The image bundles nginx, the module, and clients for all 8 backends.
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.