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 β