Skip to main content

Building a Lightning paywall for your API

How to put an L402 Lightning paywall in front of your API so AI agents pay per call in sats: the 402 handshake, macaroon caveats, and a hosted gateway.

A Lightning paywall is an API that answers unauthenticated requests with HTTP 402 Payment Required, hands back a Lightning invoice plus a signed token, and serves the response once the caller pays and returns the payment preimage. No signup, no API key issuance, no card on file, no monthly plan. The caller pays for the single call it wanted, in satoshis, and the money settles before your handler runs.

There are two ways to put one in front of your API on Lightning Faucet, and you should pick before you write any code. If you already have a working HTTP endpoint, register it with our hosted L402 gateway and you get a paid URL in one API call — we mint the invoice, verify the payment, proxy to your upstream, and credit your balance. If you want the paywall inside your own stack, implement the L402 handshake yourself; the rest of this article documents exactly how our server does it, header by header, including the parts that bite people. Either way, the paying side is the same: an agent with a Lightning wallet, such as the Lightning Wallet MCP server that gives Claude Code, Cursor, and any agent framework a sat-denominated balance.

What a Lightning paywall actually is

L402 (formerly LSAT) is a five-step handshake defined by Lightning Labs and implemented end to end on our API:

  1. Client requests a protected resource with no Authorization header.
  2. Server responds 402 with WWW-Authenticate: L402 version="0", token="<macaroon>", invoice="<bolt11>".
  3. Client pays the BOLT11 invoice and its wallet returns the preimage.
  4. Client retries with Authorization: L402 <macaroon>:<preimage>.
  5. Server verifies the macaroon signature and checks that sha256(preimage) equals the payment hash baked into the macaroon, then serves the response.

That last line is the whole trick. You never have to ask your Lightning node whether an invoice was paid. The preimage is the receipt: only someone who settled that invoice can produce a 32-byte value that hashes to the payment hash you committed to when you issued the challenge. Verification is a hash comparison in your web process, not a round trip to your node.

Why a macaroon instead of an API key

The token in the challenge is a macaroon: an identifier plus a list of caveats, chained through HMAC-SHA256 so that each caveat rehashes the running signature. On our gateway the root key comes from l402.macaroon_root_key in config, kept separate from the site encryption key. Here is a real challenge token our /api/l402/uuid endpoint issues, base64-decoded:

{
  "identifier": "84ca597f384d11a8c9baa7d8e4cb373e",
  "location": "lightningfaucet.com",
  "caveats": [
    {"type": "payment_hash", "value": "0e2b51211d9ebc832fdda746f53f35548cb855371e0305d90674792d5c9755d5"},
    {"type": "expiry", "value": 1786195335},
    {"type": "service", "value": "uuid"},
    {"type": "capabilities", "value": ["uuid_v7", "count_1"]}
  ],
  "signature": "1039ab65f7004f02001210acf7abd94c89f96cb4139704f1a46f9b3a851a1bd4"
}

Four caveats, four things a stolen or replayed token cannot do. It cannot outlive its expiry. It cannot be used on a different service than the one it paid for. It cannot exercise a capability it did not buy. And it is worthless without the preimage that matches its payment hash. An API key gives you none of that; it is a single bearer secret that means "everything, forever, until we revoke it."

Our verifier fails closed on caveat types it does not recognise. If a token arrives carrying a constraint we have no rule for, we reject it rather than ignore the constraint — the opposite default from most JWT middleware, and the right one when the token is a payment receipt.

Option A: put your existing API behind our gateway

This is the path most builders should take, because it requires zero changes to the API you already run. You register the upstream URL, a price, and optionally the headers we should send it. We return a public gateway URL. Every paid call to that URL gets proxied to your upstream, and your share is credited to your operator balance the moment the upstream responds.

Registration is one POST to https://lightningfaucet.com/api/ with your operator key:

{
  "action": "gateway_register",
  "api_key": "lf_...",
  "name": "Address Cluster Lookup",
  "description": "Returns cluster labels for a Bitcoin address",
  "upstream_url": "https://api.example.com/v1/cluster",
  "upstream_method": "POST",
  "price_sats": 120,
  "category": "data",
  "upstream_headers": {"X-Api-Key": "your-upstream-secret"},
  "upstream_timeout_ms": 8000
}

We do not take your word that the endpoint works. Registration probes the upstream first and refuses with a 422 and the probe result if it is unreachable, so a broken endpoint can never go live and start taking payments. If the probe passes, we generate a slug and the endpoint is active immediately at https://lightningfaucet.com/api/l402/gateway/<slug>.

What the gateway enforces on your behalf

Several of these limits exist because a paid proxy is an attractive target, and every one of them is checked in the live proxy path, not just at registration:

  • Private addresses are blocked, twice. Localhost, RFC1918, and link-local upstreams are rejected at registration and re-checked at proxy time, so you cannot register a public hostname and later repoint DNS at an internal service to make our servers fetch it for you.
  • Upstream secrets are encrypted at rest. The upstream_headers you supply are encrypted before they touch the database and decrypted only inside the request that uses them. If decryption ever fails, the call returns a config error rather than silently calling your upstream unauthenticated.
  • Hard ceilings. Responses over 2 MB are rejected. Timeouts are capped at 30 seconds no matter what you ask for. Prices must be between 1 and 100,000 sats. Redirects are followed at most three times, and only over HTTP or HTTPS.
  • Per-operator cap. Twenty live endpoints per operator.
  • Automatic suspension. A health-check cron probes every active endpoint and counts consecutive failures. After ten in a row the endpoint is suspended, which stops us selling access to something that is down. A single successful probe resets the counter to zero.

How you get paid

Every successful proxied call splits the price: a platform fee is deducted, and the remainder is credited to your operator balance right then, as a gateway_revenue row in your transaction ledger. You do not invoice us and you do not wait for a payout run — the balance moves per request. The split is not hidden, either. It comes back in the response envelope your caller receives:

{
  "gateway": {
    "endpoint": "address-cluster-lookup",
    "upstream_status": 200,
    "latency_ms": 412,
    "price_sats": 120,
    "platform_fee_sats": 12,
    "developer_earned_sats": 108
  },
  "upstream_response": { "...": "your API's JSON, verbatim" }
}

Note what happens when your upstream is down: the caller gets an upstream_error envelope, the request is logged, and no revenue is credited for that call. The incentive points the right way. Keep your endpoint healthy and you earn on every request; let it rot and the gateway stops paying you before the health check suspends you.

From the operator balance you can withdraw over Lightning the same way anything else on the platform moves — a withdraw link your wallet scans, or a payout to a Lightning address. Sat-denominated in, sat-denominated out.

Useful operator actions once you are live: gateway_list for your endpoints, gateway_stats for per-endpoint request and revenue counters, gateway_pause and gateway_resume to take an endpoint off sale during upstream maintenance without deleting it, and gateway_probe to test an upstream before you register it.

Option B: implement L402 inside your own stack

If you want the paywall in your own process, the shape is small enough to write in an afternoon. Ours is a middleware you call at the top of a handler with a price and a service name; if the request is unauthorized it mints an invoice, builds the macaroon, sends the 402, and exits. If it is authorized it returns and your handler runs.

protect(price_sats, service, capabilities, expiry_seconds):
    if request has Authorization header:
        macaroon, preimage = parse("L402 <macaroon>:<preimage>")
        if verify_signature(macaroon) and verify_caveats(macaroon, preimage, service):
            return AUTHORIZED
    invoice, payment_hash = create_invoice(price_sats, memo)
    macaroon = mint(payment_hash, expiry, service, capabilities)
    send 402 with WWW-Authenticate and exit

Four implementation details matter more than the rest.

Price per call, not per seat

Our own published price list is a decent calibration exercise, because it is what we charge and what agents actually pay: 5 sats for a UUID, 10 sats for a timestamp or a profanity check, 30 sats for a mempool fee heatmap or a BOLT11 decode, 50 sats for on-chain fee estimates or sentiment scoring, 100 sats for title summarisation, 200 sats for the aggregated BTC price oracle or Lightning Network statistics, 500 sats for an LLM prompt run or a question put to a panel of human judges. The pattern: price the marginal cost plus a little, because at these sizes there is no payment processor minimum dragging you toward a $5 floor. A 5 sat call is a viable product. A 5 cent card payment never was.

Token lifetime is a business decision

Our default expiry is 3,600 seconds. Within that window the same macaroon:preimage pair authorises repeated calls to the same service — that is a feature, not a leak: it means an agent pays once and then hammers the endpoint for an hour without re-invoicing on every request. If you want strict one-call-one-payment, either drop the expiry to seconds or enforce a usage-limit caveat. Be aware that a usage limit cannot be verified from the token alone; counting uses requires server-side state, so that caveat has to be checked by your application, not by the macaroon verifier.

Design the error messages for machines that cannot ask a human

This is the part nobody warns you about. Your caller is an autonomous agent. When it fails, there is no developer reading your docs — there is a retry loop. Every rejection our verifier emits carries a stable machine code and a hint that says what to do next, because we watched agents get stuck on all four of these:

  • macaroon_decode_failed — the client URL-encoded the base64 token. We now try the URL-decoded header first and fall back to the raw one, and we still say plainly: send the exact base64 string from the 402 response, unmodified.
  • preimage_missing — the client sent the macaroon with no preimage. The hint spells out the header format, colon and all.
  • preimage_invalid — the client sent the payment hash instead of the preimage, or truncated it. The hint says: use the 64-character hex string your wallet returns after the payment settles.
  • token_expired and service_mismatch — the hint includes how many seconds ago it expired, or which service the token was actually issued for.

An agent can act on those. "401 Unauthorized" with an empty body is a dead end that costs you the sale.

Publish a machine-readable menu

Agents cannot buy what they cannot find. We serve https://lightningfaucet.com/.well-known/l402.json describing every paid endpoint — path, method, price in sats, category, and required parameters — so a crawler or an agent can enumerate the catalogue and budget before it spends anything. If you run your own paywall, publish the same file. It is a flat JSON document and it is the difference between an endpoint agents can discover and one that only exists in your README.

The paying side: what your customer's agent does

Once your paywall is live, the buyer is usually not a person with a browser. It is an agent holding a Lightning balance. Ours is the Lightning Wallet MCP server, and the relevant tool is pay_l402_api: hand it a URL, a method, an optional body, and a max_payment_sats ceiling, and it makes the request, catches the 402, checks the price against the ceiling, pays, and retries with the token — all inside one tool call, with the response coming back to the model as if the endpoint had been free.

Two design choices in that client are worth copying if you build your own:

  • Operator and agent are different identities. The operator holds the funds and the recovery path; each agent gets an isolated balance funded from it. A runaway agent can only lose what it was funded with.
  • The spend ceiling is per call, not just per day. max_payment_sats is checked against the price in the 402 challenge before the invoice is paid. Combined with a budget on the agent, an unattended loop cannot quietly drain a wallet because one endpoint decided to charge 100,000 sats.

If you want to feel the whole loop as a buyer before you build as a seller, that is what the builder tools page is for: install the wallet, fund it with a few thousand sats, and call one of our 5-sat endpoints. You will see the 402, the invoice, the preimage, and the response in under a second.

Where the verification instinct comes from

We built the paywall the way we build everything else here: assume the counterparty should be able to check our work without trusting us. A Lightning preimage proving a payment is the same shape of artifact as the server seed behind a provably fair game — on dice, every result is derived from a server seed, the client seed you control, and an incrementing nonce, and you can re-hash any past round yourself once the seed is revealed. Same principle on the API: the customer holds a cryptographic receipt, and neither side has to be believed.

That is also why the whole surface is sat-denominated and settles instantly, whether the sats are moving into a prediction market position, out through a withdraw link, or into your operator balance one API call at a time. Lightning is not a bolt-on payment method here; it is the unit of account for the entire platform, which is why a 5 sat charge is worth building at all.

A build checklist

  1. Decide hosted or self-hosted. Existing HTTP endpoint and no appetite for protocol work? Hosted gateway.
  2. Price the call in sats. Start low. Marginal cost plus a margin, not a subscription divided by twelve.
  3. Register or implement. Hosted: one gateway_register call, confirm the probe passed, keep the slug. Self-hosted: mint invoice, mint macaroon with payment-hash, expiry, service and capability caveats, send the 402, verify on retry.
  4. Return a machine-readable error for every rejection, with a code and a hint.
  5. Publish .well-known/l402.json so agents can discover and budget.
  6. Test as the buyer with an agent wallet and a hard max_payment_sats ceiling.
  7. Watch the failure log. On the hosted gateway, ten consecutive failed probes suspends you — that alert is your uptime SLA with your customers.

The paywall itself is not the hard part. The hard part is deciding that a single API call is worth five sats to somebody, and then making it possible for a machine to pay that without a contract, an invoice, or a human in the loop. That is the thing Lightning changes, and you can have it in front of your API in an afternoon. Start at the builder tools, or if you would rather earn the sats to fund your first agent wallet before you spend them, the earn surfaces will get you a starting balance.

---

Written by the Lightning Faucet team, who operate the L402 gateway, the agent wallet, and the payment rails described above.

Frequently asked questions

What is an L402 Lightning paywall?

It is an API that answers unauthenticated requests with HTTP 402 Payment Required, returning a WWW-Authenticate header containing a Lightning invoice and a signed macaroon token. The caller pays the invoice, gets a preimage from its wallet, and retries with Authorization: L402 <macaroon>:<preimage>. The server checks that sha256(preimage) matches the payment hash inside the macaroon and serves the response. No account, no API key, no subscription.

Do I have to check my Lightning node to confirm the caller paid?

No, and that is the point of the design. The preimage is the receipt: only a party that settled the invoice can produce a value hashing to the payment hash you committed to in the macaroon. Verification is an HMAC signature check plus one sha256 comparison inside your web process, with no round trip to your node.

Can I paywall an API I already run without changing its code?

Yes. Register the upstream URL, method, price in sats, and any headers we should forward with the gateway_register action, and you get a public gateway URL. We probe your upstream before activating it, mint and verify the payments, proxy the call, and credit your share to your operator balance on each successful request. Your API never learns that L402 exists.

What should I charge per API call?

Price the marginal cost plus a margin rather than dividing a subscription. On our own gateway, live prices run from 5 sats for a UUID and 10 sats for a timestamp up to 200 sats for an aggregated BTC price oracle and 500 sats for an LLM prompt run. Prices between 1 and 100,000 sats are accepted. Because there is no card processor minimum, a 5 sat call is a viable product.

How long does an L402 token stay valid, and can it be reused?

Our default expiry caveat is 3,600 seconds, and within that window the same macaroon and preimage authorise repeated calls to the same service, so an agent pays once and then works for an hour. For strict one-call-one-payment, shorten the expiry or add a usage-limit caveat, but note that counting uses requires server-side state; the macaroon verifier alone cannot enforce it.

What happens if my upstream API goes down?

The caller receives an upstream_error envelope, the request is logged, and no revenue is credited for that call. A health-check cron probes every active endpoint and suspends it after ten consecutive failures, so we stop selling access to something that is not answering. One successful probe resets the counter and you can resume.

How do agents discover that my paid endpoint exists?

Publish a .well-known/l402.json document listing each path, method, price in sats, category, and required parameters. We serve one at lightningfaucet.com/.well-known/l402.json covering every paid endpoint, which lets an agent enumerate the catalogue and budget before it spends a single sat. Without it, only humans reading your README can find you.

How does an AI agent actually pay the invoice?

With a Lightning wallet it controls. Our Lightning Wallet MCP server exposes a pay_l402_api tool that takes a URL and a max_payment_sats ceiling, makes the request, catches the 402, checks the quoted price against the ceiling, pays, and retries with the token in one call. Funds sit in a per-agent balance funded by the operator account, so a runaway loop can only spend what that agent was given.