How L402 Works
This guide explains the technical details of the L402 protocol implementation in Lightning Enable.
Protocol Flow
┌──────────┐ ┌─────────────────┐ ┌─────────────┐
│ Client │ │ Lightning Enable│ │ OpenNode │
└────┬─────┘ └────────┬────────┘ └──────┬──────┘
│ │ │
│ 1. GET /api/premium/data │ │
│──────────────────────────────────>│ │
│ │ │
│ │ 2. Create Lightning Invoice │
│ │───────────────────────────────────>│
│ │ │
│ │ 3. Invoice + Payment Hash │
│ │<───────────────────────────────────│
│ │ │
│ 4. HTTP 402 Payment Required │ │
│ WWW-Authenticate: L402 │ │
│ macaroon="...", invoice="..." │ │
│<──────────────────────────────────│ │
│ │ │
│ 5. Pay invoice (via any wallet) │ │
│─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─>│
│ │ │
│ 6. Preimage (proof of payment) │ │
│<─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─│
│ │ │
│ 7. GET /api/premium/data │ │
│ Authorization: L402 mac:preim │ │
│──────────────────────────────────>│ │
│ │ │
│ │ 8. Verify SHA256(preimage)==hash │
│ │ Verify macaroon signature │
│ │ │
│ 9. HTTP 200 OK (response) │ │
│<──────────────────────────────────│ │
Key Concepts
1. Lightning Invoice (BOLT11)
When a client requests a protected endpoint, Lightning Enable creates a Lightning invoice:
lnbc100n1pnxyzabc... (encoded invoice)
The invoice contains:
- Amount in satoshis
- Payment hash (SHA256 of a secret preimage)
- Expiry time
- Destination (OpenNode's node)
2. Payment Hash & Preimage
The payment hash is the key to L402:
preimage (32 bytes, secret) → SHA256 → payment_hash (32 bytes, public)
- The payment hash is included in the invoice
- The preimage is revealed when the invoice is paid
- Knowing the preimage proves payment was made
3. Macaroon
A macaroon is a cryptographic bearer token containing:
{
"identifier": "lightning-enable:payment_hash:expires",
"caveats": [
"services = lightning-enable:0",
"path = /api/premium/*",
"expires = 1704067200"
],
"signature": "hmac-sha256-signature"
}
Caveats can restrict:
- Which services/paths are accessible
- When the token expires
- Additional custom conditions
4. L402 Credential
The client combines macaroon and preimage:
Authorization: L402 <base64-macaroon>:<hex-preimage>
Verification Process
When Lightning Enable receives an L402 credential:
Step 1: Parse Credential
const [scheme, credential] = authHeader.split(' ');
const [macaroon, preimage] = credential.split(':');
Step 2: Verify Preimage
// Extract payment hash from macaroon
const paymentHash = extractPaymentHash(macaroon);
// Compute hash of preimage
const computedHash = sha256(hexToBytes(preimage));
// Verify match
if (computedHash !== paymentHash) {
throw new Error('Preimage does not match payment hash');
}
Step 3: Verify Macaroon Signature
// Verify macaroon wasn't tampered with
const isValid = verifyMacaroonSignature(macaroon, rootKey);
if (!isValid) {
throw new Error('Invalid macaroon signature');
}
Step 4: Check Caveats
// Verify all caveats are satisfied
const caveats = extractCaveats(macaroon);
// Check expiration
if (caveats.expires < Date.now()) {
throw new Error('Token expired');
}
// Check path access
if (!pathMatches(requestPath, caveats.path)) {
throw new Error('Path not authorized');
}
Payment Hash Extraction
Lightning Enable extracts the payment hash directly from BOLT11 invoices:
private byte[]? ExtractPaymentHashFromBolt11(string invoice)
{
// Find the '1' separator between human-readable and data parts
var separatorIndex = invoice.LastIndexOf('1');
var dataPart = invoice.Substring(separatorIndex + 1);
// Skip timestamp (first 7 chars)
dataPart = dataPart.Substring(7);
// Find tagged field 'p' (payment hash)
// Tag 'p' = 1, followed by data length, followed by 52 bech32 chars
// 52 bech32 chars * 5 bits = 260 bits = 256 bits (32 bytes) + padding
var paymentHash = ParseTaggedField(dataPart, 'p');
return paymentHash; // 32 bytes
}
Token Caching
For performance, verified tokens are cached:
public class L402TokenCache
{
private readonly IMemoryCache _cache;
private readonly TimeSpan _cacheDuration = TimeSpan.FromMinutes(5);
public bool TryGetVerified(string preimage, out L402Token token)
{
return _cache.TryGetValue(preimage, out token);
}
public void CacheVerified(string preimage, L402Token token)
{
_cache.Set(preimage, token, _cacheDuration);
}
}
Multi-Use Tokens
A single L402 payment can be used for multiple requests during the token validity period:
- Client pays once
- Receives preimage
- Uses same macaroon:preimage for subsequent requests
- Token valid until expiration (configurable, default 1 hour)
Security Considerations
Preimage Security
- Treat preimages like passwords
- Don't log full preimages
- Use HTTPS to prevent interception
Macaroon Tampering
- Macaroons are signed with HMAC-SHA256
- Root key must be kept secret
- Any modification invalidates the signature
Token Expiration
- Configure appropriate validity periods
- Shorter = more secure, but more payments needed
- Longer = better UX, but higher risk if compromised
Rate Limiting
Even with valid payments, implement rate limiting:
// Limit requests per payment hash
services.AddRateLimiter(options =>
{
options.AddPolicy("L402", httpContext =>
{
var paymentHash = GetPaymentHash(httpContext);
return RateLimitPartition.GetFixedWindowLimiter(
paymentHash,
_ => new FixedWindowRateLimiterOptions
{
PermitLimit = 100,
Window = TimeSpan.FromHours(1)
});
});
});
Configuration
L402 Settings
{
"L402": {
"Enabled": true,
"ServiceName": "my-api",
"DefaultPriceSats": 100,
"DefaultTokenValiditySeconds": 3600,
"InvoiceExpirySeconds": 600,
"CacheVerifiedTokens": true,
"TokenCacheSeconds": 300
}
}
Protected Paths
{
"L402": {
"ProtectedPaths": [
"/api/premium/*",
"/api/ai/*"
],
"ExcludedPaths": [
"/api/public/*",
"/health"
]
}
}
Endpoint Pricing
{
"L402": {
"EndpointPricing": [
{ "PathPattern": "/api/ai/gpt4", "PriceSats": 500 },
{ "PathPattern": "/api/ai/dalle", "PriceSats": 1000 },
{ "PathPattern": "/api/premium/*", "PriceSats": 50 }
]
}
}
Error Responses
402 Payment Required
{
"error": "Payment Required",
"message": "Pay the Lightning invoice to access this resource",
"l402": {
"macaroon": "AgEL...",
"invoice": "lnbc100n1p3...",
"amount_sats": 100,
"payment_hash": "abc123...",
"expires_at": "2024-12-29T13:00:00Z"
}
}
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"
}
Next Steps
- API Monetization - Protect your endpoints
- Proxy Configuration - Monetize any API
- API Reference - Complete L402 API docs