# Built a Lightning-gated reverse proxy that charges AI scrapers

> Source: <https://promptcube3.com/en/news/7157/>
> Published: 2026-08-21 07:40:47+00:00

# Built a Lightning-gated reverse proxy that charges AI scrapers

The stack is deliberately minimal — a Go proxy that intercepts inbound traffic, validates an L402 (LSAT + Lightning) credential, and either forwards the request or returns a `402 Payment Required`

with a fresh invoice. No accounts, no API keys, no Stripe. Just sats.

```
client → [Argentic] → your API
           │
           └─ validates macaroon + preimage
              │
              ├─ valid → proxy request
              └─ invalid → 402 + lightning invoice
```

Each macaroon carries caveats: target path, method, expiry timestamp, max response bytes. The preimage proves payment settled on-chain (well, off-chain via Lightning). Caveats are verified cryptographically — no database lookup needed.

Deployment is a single binary plus a config file:

```
listen: ":8080"
upstream: "http://api.internal:8000"
lnd:
  host: "lnd:10009"
  macaroon_path: "/data/admin.macaroon"
  tls_path: "/data/tls.cert"
pricing:
  default: 1000  # millisats per request
  paths:
    "/v1/premium": 5000
    "/v1/bulk": 100
macaroon:
  expiry: "24h"
  id_bytes: 16
```

The pricing model is where it gets interesting. You can charge more for compute-heavy endpoints, less for cached reads, zero for health checks. Since the macaroon encodes the path, a single invoice can cover a batch of requests to the same tier — the agent presents the same preimage until expiry.

Tested it against a few open-source scraping frameworks. Most choke on `402`

because they expect `429`

or `403`

. Had to patch `httpx`

and `aiohttp`

middleware to auto-pay and retry. That friction is the feature — it filters for agents that actually have a budget.

One gotcha: LND's `settleInvoice`

RPC requires the preimage, but the proxy only sees the payment hash in the macaroon. Workaround is a background poller that indexes settled invoices by payment hash → preimage. Adds ~200ms latency on first request after payment. Acceptable for now.

```
func (p *Proxy) validateMacaroon(m *macaroon.Macaroon, preimage []byte) error {
    // verify signature with root key
    if !m.Verify(p.rootKey) {
        return ErrInvalidSignature
    }
    // check caveats
    for _, c := range m.Caveats() {
        if !p.checkCaveat(c, preimage) {
            return ErrCaveatFailed
        }
    }
    // verify preimage hashes to payment_hash in macaroon
    if !bytes.Equal(sha256.Sum256(preimage), m.PaymentHash()) {
        return ErrPreimageMismatch
    }
    return nil
}
```

Still deciding on the macaroon rotation strategy. Short expiry (1h) means frequent re-payment but tighter revocation. Long expiry (24h) reduces Lightning traffic but leaves a wider window if a preimage leaks. Leaning toward 4h with a refresh endpoint that issues a new macaroon for the same preimage.

Open question: should the proxy aggregate multiple requests into a single invoice (pay once, get N requests) or keep it strictly per-request? Per-request is simpler and maps cleanly to metered API pricing. Batch feels better for high-frequency agents but complicates caveat encoding.

Binary and config examples at the repo. No Docker image yet — `go build`

and drop it in front of whatever you're protecting. Works with any LND-compatible node (Core Lightning, LND, LDK).

What's the most hostile scraping pattern you've seen that rate limits didn't stop?

[Next Gen Z job anxiety hits new highs as AI coding agents ship →](/en/news/7155/)
