@satpath/gateless
v0.4.3
Published
Sovereign L402 on Lightning - client for AI agents, server middleware for merchants, browser-compatible NWC
Maintainers
Readme
Gateless
Sovereign L402 payments on Lightning - both sides of the protocol, in one JavaScript library.
Gateless is a TypeScript library for the L402 Lightning payment protocol:
- Client - a drop-in
fetch()replacement so AI agents in Node.js can autonomously pay for L402-protected resources. Supports classic L402 and Fewsats v0.2. - Server - a tiny middleware to monetise your own HTTP APIs with Lightning paywalls. Issues challenges, verifies tokens, plugs into any framework whose request/response objects duck-type like Express's.
Connect your own LND node or any NWC-compatible wallet (Alby Hub, Mutiny, etc.). No custodian, no accounts, no API keys - payment is authentication.
Why
AI agents need to pay for things, and the people serving them need to get paid. The emerging solutions either lock both sides into custodial stablecoins (x402) or require shell access to Go CLI tools (lnget) and separate Go binaries (Aperture). Neither works for a web developer shipping in TypeScript.
Gateless fills both gaps in one package: a self-contained JS/TS toolkit that connects to your own LND node or any NWC-compatible wallet and handles the full L402 flow in both directions. No custodian, no corporate infrastructure, your keys.
Install
npm install @satpath/gatelessQuick Start
import { LndClient, L402Client } from "@satpath/gateless";
const lnd = new LndClient({
host: "127.0.0.1",
port: 8080,
tlsCertPath: "./creds/tls.cert",
macaroonPath: "./creds/invoice.macaroon",
});
const client = new L402Client({
paymentProvider: lnd,
maxPaymentSats: 100,
spendingLimits: {
maxPerPaymentSats: 50,
maxTotalSats: 1000,
maxPaymentsPerMinute: 10,
},
});
// Use it like fetch - L402 payments happen automatically
const response = await client.fetch("https://api.example.com/paid-resource");
const data = await response.json();Using NWC (Nostr Wallet Connect)
No LND node? Use any NWC-compatible wallet (Alby Hub, Mutiny, etc.) instead:
import { NwcClient, L402Client } from "@satpath/gateless";
const nwc = new NwcClient({
connectionString: "nostr+walletconnect://pubkey?relay=wss://...&secret=hex",
});
const client = new L402Client({
paymentProvider: nwc,
maxPaymentSats: 100,
});
const response = await client.fetch("https://api.example.com/paid-resource");
const data = await response.json();
// When done, disconnect from the relay
nwc.close();Get the connection string from your wallet's NWC settings (usually under "App Connections" or "Nostr Wallet Connect").
NwcClient works in the browser as of v0.4.2 - it detects globalThis.WebSocket and falls back to the ws package only in Node environments that don't have native WebSocket. SHA-256 derivation uses globalThis.crypto.subtle (available in all browsers and Node 18.17+). Bundlers alias ws out via the browser field in our package.json, and "sideEffects": false lets them tree-shake the server-only modules out of browser builds.
If the server returns 402 Payment Required, Gateless automatically detects the protocol version and handles payment:
Classic L402 - Parses the WWW-Authenticate header, pays the invoice, caches the macaroon+preimage token, and retries with Authorization: L402. Second requests reuse the cached token.
Fewsats v0.2 - Parses the JSON offers body, selects an offer, fetches a Lightning invoice from the payment request endpoint, pays it, and retries with the original request headers (Bearer token). The server credits your account after payment.
Fewsats v0.2
Most live L402 endpoints today use the Fewsats v0.2 protocol. Your Bearer token (obtained separately) is your credential - Gateless handles the payment when the server returns 402:
const client = new L402Client({
paymentProvider: lnd,
maxPaymentSats: 100,
});
// Bearer token is passed through - Gateless pays when the server demands it
const response = await client.fetch("https://api.example.com/paid-resource", {
headers: { Authorization: "Bearer your-api-token" },
});
const data = await response.json();You can customize which offer is selected when multiple are available:
import { L402Client, type OfferStrategy } from "@satpath/gateless";
const pickMostCredits: OfferStrategy = (offers) => {
const lightning = offers.filter((o) =>
o.payment_methods.includes("lightning"),
);
if (lightning.length === 0) throw new Error("No lightning offers");
lightning.sort((a, b) => b.balance - a.balance);
return lightning[0]!;
};
const client = new L402Client({
paymentProvider: lnd,
maxPaymentSats: 100,
offerStrategy: pickMostCredits, // default: cheapest lightning-compatible offer
});Monetise Your Own API (Server)
The @satpath/gateless/server subpath turns any HTTP endpoint into an L402 paywall - the server-side mirror of the client. Five lines of Express:
import express from "express";
import { LndClient } from "@satpath/gateless";
import { L402Server } from "@satpath/gateless/server";
const lnd = new LndClient({
host: "127.0.0.1",
port: 8080,
tlsCertPath: "./creds/tls.cert",
macaroonPath: "./creds/invoice.macaroon",
});
const l402 = new L402Server({
secret: process.env.L402_SECRET!, // HMAC key used to sign macaroons
invoiceProvider: lnd,
});
const app = express();
app.get("/premium", l402.protect({ priceSats: 100 }), (_req, res) => {
res.json({ data: "paid content" });
});LndClient and NwcClient both implement InvoiceProvider as well as PaymentProvider - the same wallet can send and receive. Swap the provider to receive via NWC:
import { NwcClient } from "@satpath/gateless";
const nwc = new NwcClient({ connectionString: "nostr+walletconnect://..." });
const l402 = new L402Server({
secret: process.env.L402_SECRET!,
invoiceProvider: nwc,
});protect() returns a standard (req, res, next) middleware that works with Express, Hono (Node adapter), Fastify, and anything else that duck-types the same shape. For lower-level control use the core methods directly:
const challenge = await l402.issueChallenge(100);
// { macaroon, invoice, paymentHash, wwwAuthenticate, status: 402 }
const result = l402.verifyAuthorization(req.headers.authorization);
// { ok: true, paymentHash, priceSats } | { ok: false, reason }Macaroons are HMAC-signed with your secret and carry the invoice's payment hash as a caveat. Verification checks the signature and confirms sha256(preimage) === paymentHash - no database lookup, no invoice-state tracking. Stateless merchants.
Features
L402 Client - Drop-in fetch wrapper that handles the full 402 → pay → retry flow automatically. Supports both classic L402 and Fewsats v0.2.
Fewsats v0.2 - Automatic offer selection, payment request negotiation, and Bearer token auth. Pluggable offer strategy (default: cheapest lightning-compatible).
Token Cache - Stores paid macaroon+preimage pairs for classic L402. Avoids double-paying for the same resource. Supports optional TTL expiry.
Spending Controls - Set per-payment limits, total budgets, and rate limits. Applies to both classic and v0.2 flows. An AI agent physically cannot exceed the budget you define.
Payment Provider Interface - Ships with LND (REST) and NWC (Nostr Wallet Connect) providers. Bring your own by implementing a simple interface:
interface PaymentProvider {
payInvoice(paymentRequest: string): Promise<PaymentResult>;
}L402 Server - Mirror middleware for monetising your own APIs. Issues real BOLT11 invoices via LndClient or NwcClient, signs opaque macaroons with HMAC, verifies preimages statelessly. Plugs into Express, Hono, Fastify via a duck-typed (req, res, next) signature. Bring-your-own InvoiceProvider for CLN or custom backends.
How L402 Works
L402 is a protocol that uses Lightning Network payments for authentication. Gateless supports two variants:
Classic L402
The server returns a macaroon and invoice in the WWW-Authenticate header. After payment, the preimage proves you paid.
Agent Server
| GET /resource |
|----------------------------->|
| 402 + WWW-Authenticate: |
| L402 macaroon="...", |
| invoice="lnbc..." |
|<-----------------------------|
| [pays Lightning invoice] |
| GET /resource |
| Authorization: L402 |
| <macaroon>:<preimage> |
|----------------------------->|
| 200 OK { data } |
|<-----------------------------|Fewsats v0.2
The server returns a JSON body with offers. The agent selects an offer, fetches a Lightning invoice, pays it, and retries with the original Bearer token.
Agent Server Payment Endpoint
| GET /resource | |
| Authorization: Bearer ... | |
|----------------------------->| |
| 402 + JSON body: | |
| { offers, payment_ | |
| context_token, | |
| payment_request_url } | |
|<-----------------------------| |
| POST /payment-request | |
| { offer_id, |-------------------->|
| payment_method, | |
| payment_context_token } | |
| { lightning_invoice } |<--------------------|
|<-----------------------------| |
| [pays Lightning invoice] | |
| GET /resource | |
| Authorization: Bearer ... | |
|----------------------------->| |
| 200 OK { data } | |
|<-----------------------------| |No accounts. No passwords. No tracking. Payment is the authentication.
Architecture
Client (paying side)
┌─────────────────────────────────────────┐
│ Your Application │
│ │
│ const res = await client.fetch(url) │
└────────────────┬────────────────────────┘
│
┌────────────────▼────────────────────────┐
│ L402Client │
│ │
│ ┌──────────┐ ┌──────────┐ ┌─────────┐ │
│ │ Token │ │ Spending │ │ Invoice │ │
│ │ Cache │ │ Tracker │ │ Decoder │ │
│ └──────────┘ └──────────┘ └─────────┘ │
└────────────────┬────────────────────────┘
│
┌────────────────▼────────────────────────┐
│ PaymentProvider │
│ │
│ LndClient │ NwcClient │
│ REST API │ Nostr Wallet Connect │
└───────┬─────────────┬───────────────────┘
│ │
┌───────▼───────┐ ┌───▼──────────────────┐
│ Your LND Node │ │ NWC Wallet │
│ (your keys) │ │ (Alby Hub, etc.) │
└───────────────┘ └──────────────────────┘Server (receiving side)
┌─────────────────────────────────────────┐
│ Your Express / Hono / Fastify App │
│ │
│ app.get("/premium", l402.protect()) │
└────────────────┬────────────────────────┘
│
┌────────────────▼────────────────────────┐
│ L402Server │
│ │
│ ┌──────────────┐ ┌─────────────────┐ │
│ │ Macaroon │ │ Preimage │ │
│ │ Signer │ │ Verifier │ │
│ └──────────────┘ └─────────────────┘ │
└────────────────┬────────────────────────┘
│
┌────────────────▼────────────────────────┐
│ InvoiceProvider │
│ │
│ LndClient │ NwcClient │
│ /v1/invoices │ NIP-47 make_invoice │
└───────┬─────────────┬───────────────────┘
│ │
┌───────▼───────┐ ┌───▼──────────────────┐
│ Your LND Node │ │ NWC Wallet │
│ (your keys) │ │ (Alby Hub, etc.) │
└───────────────┘ └──────────────────────┘Comparison
| | Gateless | lnget (Lightning Labs) | x402 (Coinbase) | | ----------------- | ------------------- | ---------------------- | ---------------------- | | Language | TypeScript | Go | Multiple | | Runtime | Node.js | CLI only | Server SDKs | | Payment rail | Lightning (Bitcoin) | Lightning (Bitcoin) | USDC (stablecoins) | | Node | Your own LND or NWC | Your own LND | Coinbase custody | | Identity required | No | No | Yes (Coinbase account) | | npm install | Yes | No | Yes | | Self-sovereign | Yes | Yes | No |
Gateless and lnget are complementary. lnget is for terminal-based agents (Claude Code, Codex). Gateless is for web developers building agents in TypeScript.
Roadmap
- ✅ LND REST payment provider
- ✅ L402 fetch client with automatic payment
- ✅ Token caching
- ✅ Spending limits and rate controls
- ✅ Fewsats L402 v0.2 support (offers, payment requests, pluggable offer strategy)
- ✅ Nostr Wallet Connect (NWC) payment provider
- ✅ Server-side middleware (Aperture alternative in JS, LND + NWC)
- ✅ Browser-compatible NwcClient (v0.4.2 - pay invoices from the browser via any NWC wallet)
- ⬜ WebLN provider (complement to browser NWC)
- ⬜ Server-side Fewsats v0.2 (offers + payment_context_token)
- ⬜ Lightning Node Connect (LNC) provider
- ⬜ Core Lightning (CLN) provider
- ⬜ Nostr endpoint discovery
- ⬜ React hooks (
useL402Fetch) - ⬜ Macaroon attenuation and inspection
Requirements
- Node.js 18+
- One of:
- An LND node (v0.16+) with REST API enabled and a funded Lightning channel, or
- An NWC connection string from any NWC-compatible wallet (Alby Hub, etc.)
LND Credentials
If using LND directly, Gateless needs two files from your node:
- TLS certificate - usually at
~/.lnd/tls.cert - Macaroon - Gateless only needs permission to pay invoices. For best security, bake a restricted macaroon rather than using the admin macaroon:
# Recommended: create a macaroon with only the permissions Gateless needs
lncli bakemacaroon invoices:read offchain:write --save_to invoice.macaroon
# admin.macaroon works too but grants far broader access than necessaryCopy them to your project (e.g. a creds/ directory) and point LndClient at them. Treat both files as secrets and don't commit them to version control or bake them into container images. If your node is on a different machine, use an SSH tunnel to forward the REST port:
ssh -L 8080:127.0.0.1:8080 user@your-node-ipNWC Connection
If using NWC, you just need a connection string from your wallet. No files to copy, no ports to forward. The connection goes through a Nostr relay, so your wallet can be anywhere.
⚠️ Disclaimer
Gateless is experimental software. It interacts with real Bitcoin on the Lightning Network. By using this software you accept full responsibility for any funds sent or lost. Always start with small amounts, use spending limits, and test thoroughly before deploying in any production environment. This software is provided as-is with no warranty of any kind.
License
MIT
