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 signed with HMAC-SHA256. Lightning Enable embeds a set of caveats (restrictions) into every macaroon at issuance time. These caveats bind the token to the exact context it was created for:
{
"identifier": "lightning-enable:payment_hash:expires",
"caveats": [
"services = lightning-enable:0",
"path = /api/premium/data",
"merchant_id = 42",
"charge_id = abc123-def456",
"amount_sats = 100",
"expires = 1704067200"
],
"signature": "hmac-sha256-signature"
}
Each caveat enforces a specific security constraint:
| Caveat | Purpose |
|---|---|
path | Binds the token to the API path it was issued for. Supports exact match or wildcard prefix (e.g., /l402/proxy/my-api/*). |
merchant_id | Binds the token to the issuing merchant, preventing cross-tenant token reuse. |
amount_sats | Binds the token to the price at issuance, preventing reuse at a different price tier. |
expires | Sets the token expiration as a Unix timestamp (default: 1 hour). |
charge_id | Records the OpenNode charge ID for the associated payment. |
services | Identifies the service name and tier. |
All caveats are verified on every request. Any unrecognized caveat causes verification to fail (closed-world assumption), ensuring forward compatibility and defense in depth.
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 binding
if (!pathMatches(requestPath, caveats.path)) {
throw new Error('Token not valid for this path');
}
// Check merchant isolation
if (caveats.merchant_id !== requestMerchantId) {
throw new Error('Token not valid for this merchant');
}
// Check price tier
if (caveats.amount_sats !== endpointPriceSats) {
throw new Error('Token amount mismatch');
}
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
Caveat-Based Token Binding
Macaroon caveats are the primary defense against token misuse. Lightning Enable enforces caveats that prevent three categories of attack:
Path binding (path caveat) -- A token issued for /api/premium/v1 cannot be used to access /api/premium/v2. This prevents clients from paying for a cheap endpoint and reusing the token against an expensive one. Wildcard paths (e.g., /l402/proxy/my-api/*) allow sub-path access when appropriate.
Merchant isolation (merchant_id caveat) -- In Lightning Enable's multi-tenant architecture, each merchant operates independently. The merchant_id caveat prevents a token issued by Merchant A from being replayed against Merchant B's endpoints. This is enforced bidirectionally: if a request carries a merchant context, the token must contain a matching merchant_id, and if a token contains a merchant_id, the request must have a matching merchant context.
Price tier enforcement (amount_sats caveat) -- A token purchased at 10 sats for a demo endpoint cannot be reused against a 100-sat premium endpoint, even if both endpoints share a wildcard path pattern. The server compares the token's amount_sats caveat against the current endpoint's configured price and rejects mismatches.
Unknown caveat rejection -- Any caveat the server does not recognize causes verification to fail. This closed-world approach ensures that if new caveat types are added in the future, older verification logic will not silently skip them.
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 (
L402_ROOT_KEYenvironment variable) - 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