L402 API
L402 (formerly LSAT) enables pay-per-request API access using Lightning Network payments.
Overview
L402 is a protocol for HTTP 402 Payment Required responses that enables:
- Pay-per-request API access without subscriptions
- Anonymous access - no accounts or credit cards needed
- Instant micropayments via Lightning Network
- Cryptographic verification using macaroons and preimages
Endpoints
Get L402 Pricing
Get pricing information for L402-protected endpoints.
GET /api/l402/pricing
Response
{
"defaultPriceSats": 10,
"serviceName": "lightning-enable",
"endpoints": [
{
"pathPattern": "/api/premium/*",
"priceSats": 100,
"description": "Premium API access"
},
{
"pathPattern": "/l402/proxy/*",
"priceSats": "varies",
"description": "Proxy pricing set per-proxy"
}
],
"tokenValiditySeconds": 3600
}
Check L402 Status
Check if a request has valid L402 authentication.
GET /api/l402/status
Headers
| Header | Required | Description |
|---|---|---|
Authorization | No | L402 <macaroon>:<preimage> |
Response (Authenticated)
{
"authenticated": true,
"paymentHash": "abc123...",
"expiresAt": "2024-12-29T13:00:00Z",
"remainingRequests": null
}
Response (Not Authenticated)
{
"authenticated": false,
"message": "L402 credential required"
}
L402 Protected Proxy
Access proxied APIs with L402 payment.
* /l402/proxy/{proxyId}/{path}
Without L402 Credential
Returns 402 Payment Required:
{
"error": "Payment Required",
"message": "Pay the Lightning invoice to access this resource",
"l402": {
"macaroon": "AgELbGlnaHRuaW5nLWVuYWJsZQJCMDAwMDAwMD...",
"invoice": "lnbc100n1pnxyz...",
"amount_sats": 10,
"payment_hash": "abc123def456...",
"expires_at": "2024-12-29T13:00:00Z"
}
}
With Valid L402 Credential
curl https://api.lightningenable.com/l402/proxy/{proxyId}/endpoint \
-H "Authorization: L402 AgEL...:abc123..."
Returns the proxied API response.
L402 Authentication Flow
Step 1: Request Protected Resource
curl https://api.lightningenable.com/l402/proxy/my-api/data
Step 2: Receive 402 Challenge
HTTP/1.1 402 Payment Required
WWW-Authenticate: L402 macaroon="AgEL...", invoice="lnbc..."
{
"error": "Payment Required",
"l402": {
"macaroon": "AgEL...",
"invoice": "lnbc100n1p...",
"amount_sats": 10,
"payment_hash": "abc123..."
}
}
Step 3: Pay Lightning Invoice
Pay the invoice using any Lightning wallet. You'll receive a preimage (proof of payment).
Step 4: Access with Credential
Combine macaroon and preimage:
curl https://api.lightningenable.com/l402/proxy/my-api/data \
-H "Authorization: L402 AgEL...:abc123def456789..."
Step 5: Receive Response
HTTP/1.1 200 OK
{
"data": "Your requested content"
}
Credential Format
The L402 credential consists of two parts:
Authorization: L402 <macaroon>:<preimage>
| Component | Format | Description |
|---|---|---|
macaroon | Base64 | Bearer token with caveats |
preimage | Hex (64 chars) | 32-byte proof of payment |
Verification
The server verifies:
- Preimage matches hash:
SHA256(preimage) == payment_hash - Macaroon signature: Token wasn't tampered with
- Caveats satisfied: Token not expired, correct path, etc.
Token Reuse
L402 tokens can be reused until they expire:
// Save credential after first payment
const credential = `${macaroon}:${preimage}`;
localStorage.setItem('l402_credential', credential);
// Reuse for subsequent requests
const savedCredential = localStorage.getItem('l402_credential');
fetch(url, {
headers: { 'Authorization': `L402 ${savedCredential}` }
});
Default token validity: 1 hour (configurable per endpoint)
Macaroon Structure
Macaroons contain:
{
"identifier": "lightning-enable:payment_hash:timestamp",
"caveats": [
"services = lightning-enable:0",
"path = /l402/proxy/my-api/*",
"expires = 1704067200"
],
"signature": "hmac-sha256"
}
Caveat Types
| Caveat | Description |
|---|---|
services | Service identifier and tier |
path | Allowed request paths |
expires | Expiration timestamp |
ip | Client IP restriction (optional) |
Error Responses
402 Payment Required
{
"error": "Payment Required",
"message": "Pay the Lightning invoice to access this resource",
"l402": {
"macaroon": "...",
"invoice": "...",
"amount_sats": 10
}
}
401 Invalid Credential
{
"error": "Unauthorized",
"message": "Invalid L402 credential",
"details": "Preimage does not match payment hash"
}
403 Token Expired
{
"error": "Forbidden",
"message": "L402 token has expired",
"details": "Token expired at 2024-12-29T12:00:00Z"
}
403 Path Not Allowed
{
"error": "Forbidden",
"message": "Token not valid for this path",
"allowed": "/l402/proxy/api-a/*",
"requested": "/l402/proxy/api-b/data"
}
Code Examples
JavaScript L402 Client
class L402Client {
constructor() {
this.credentials = new Map();
}
async request(url, options = {}) {
const credential = this.credentials.get(this.getHost(url));
const headers = {
...options.headers,
...(credential && { 'Authorization': `L402 ${credential}` })
};
const response = await fetch(url, { ...options, headers });
if (response.status === 402) {
return this.handlePaymentRequired(url, response, options);
}
return response;
}
async handlePaymentRequired(url, response, options) {
const { l402 } = await response.json();
// Pay invoice and get preimage
const preimage = await this.payInvoice(l402.invoice);
// Store credential
const credential = `${l402.macaroon}:${preimage}`;
this.credentials.set(this.getHost(url), credential);
// Retry request
return this.request(url, options);
}
async payInvoice(invoice) {
// Integrate with your Lightning wallet
// Return the preimage after payment
throw new Error('Implement payInvoice()');
}
getHost(url) {
return new URL(url).host;
}
}
// Usage
const client = new L402Client();
const response = await client.request('https://api.example.com/l402/proxy/my-api/data');
Python L402 Client
import hashlib
import requests
class L402Client:
def __init__(self, pay_invoice_callback):
self.credentials = {}
self.pay_invoice = pay_invoice_callback
def request(self, url, **kwargs):
credential = self.credentials.get(self._get_host(url))
if credential:
kwargs.setdefault('headers', {})
kwargs['headers']['Authorization'] = f'L402 {credential}'
response = requests.request('GET', url, **kwargs)
if response.status_code == 402:
return self._handle_payment_required(url, response, kwargs)
return response
def _handle_payment_required(self, url, response, kwargs):
data = response.json()
l402 = data['l402']
# Pay invoice
preimage = self.pay_invoice(l402['invoice'])
# Verify preimage matches
payment_hash = hashlib.sha256(bytes.fromhex(preimage)).hexdigest()
assert payment_hash == l402['payment_hash']
# Store and retry
self.credentials[self._get_host(url)] = f"{l402['macaroon']}:{preimage}"
return self.request(url, **kwargs)
def _get_host(self, url):
from urllib.parse import urlparse
return urlparse(url).netloc
cURL Workflow
#!/bin/bash
# Step 1: Get challenge
RESPONSE=$(curl -s https://api.example.com/l402/proxy/my-api/data)
HTTP_CODE=$(echo "$RESPONSE" | jq -r '.error // empty')
if [ "$HTTP_CODE" == "Payment Required" ]; then
MACAROON=$(echo "$RESPONSE" | jq -r '.l402.macaroon')
INVOICE=$(echo "$RESPONSE" | jq -r '.l402.invoice')
echo "Pay this invoice: $INVOICE"
echo "Enter preimage after payment:"
read PREIMAGE
# Step 2: Access with credential
curl https://api.example.com/l402/proxy/my-api/data \
-H "Authorization: L402 $MACAROON:$PREIMAGE"
fi
Wallet Integration
WebLN (Browser)
async function payL402Invoice(invoice) {
if (!window.webln) {
throw new Error('WebLN not available');
}
await window.webln.enable();
const { preimage } = await window.webln.sendPayment(invoice);
return preimage;
}
LND REST API
async function payWithLND(invoice) {
const response = await fetch(`${LND_REST_URL}/v1/channels/transactions`, {
method: 'POST',
headers: { 'Grpc-Metadata-macaroon': ADMIN_MACAROON },
body: JSON.stringify({ payment_request: invoice })
});
const { payment_preimage } = await response.json();
return Buffer.from(payment_preimage, 'base64').toString('hex');
}
Core Lightning
# Pay and get preimage
lightning-cli pay $INVOICE
PREIMAGE=$(lightning-cli listpays bolt11=$INVOICE | jq -r '.pays[0].preimage')
Best Practices
Store Credentials
Cache L402 credentials for token lifetime:
const CREDENTIAL_KEY = 'l402_credentials';
function saveCredential(host, credential, expiresAt) {
const credentials = JSON.parse(localStorage.getItem(CREDENTIAL_KEY) || '{}');
credentials[host] = { credential, expiresAt };
localStorage.setItem(CREDENTIAL_KEY, JSON.stringify(credentials));
}
function getCredential(host) {
const credentials = JSON.parse(localStorage.getItem(CREDENTIAL_KEY) || '{}');
const data = credentials[host];
if (data && new Date(data.expiresAt) > new Date()) {
return data.credential;
}
return null;
}
Handle Expired Tokens
async function request(url) {
const response = await fetch(url, {
headers: { 'Authorization': `L402 ${getCredential(url)}` }
});
if (response.status === 403) {
// Token expired, clear and get new one
clearCredential(url);
return request(url);
}
return response;
}
Budget Limits
Set spending limits:
class BudgetedL402Client extends L402Client {
constructor(maxSatsPerHour) {
super();
this.maxSats = maxSatsPerHour;
this.spent = 0;
this.resetTime = Date.now() + 3600000;
}
async handlePaymentRequired(url, response, options) {
const { l402 } = await response.json();
if (Date.now() > this.resetTime) {
this.spent = 0;
this.resetTime = Date.now() + 3600000;
}
if (this.spent + l402.amount_sats > this.maxSats) {
throw new Error('Budget exceeded');
}
this.spent += l402.amount_sats;
return super.handlePaymentRequired(url, response, options);
}
}
Next Steps
- Proxy Configuration - Set up L402 proxies
- How It Works - Technical deep dive
- API Monetization - Protect your APIs