How L402 Works
This guide explains the technical details of the L402 protocol implementation in Lightning Enable.
Protocol Flowβ
ββββββββββββ βββββββββββββββββββ βββββββββββββββ
β Client β β Lightning Enableβ β Provider β
ββββββ¬ββββββ ββββββββββ¬βββββββββ ββββββββ¬βββββββ
β β β
β 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) β β
β<βββββββββββββββββββββββββββββββββββ β
Provider is the merchant's configured payment provider β Strike (the default) or OpenNode. Lightning Enable talks to whichever one the merchant selected; the L402 flow is identical either way. Lightning Enable does not hold funds β the payment provider facilitates custody and settlement.
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 (the payment provider's node β Strike or OpenNode, per merchant configuration)
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 payment provider's charge ID (Strike or OpenNode) 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 per proxy in the dashboard wizard, falling back to the global 1-hour default
Per-proxy token validity was fixed in the July 2026 update; earlier tokens always used the 1-hour default.
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β
The JSON blocks below (L402 options: ProtectedPaths, EndpointPricing, etc.) are Lightning Enable's own server-side settings β they configure the hosted service itself, and merchants cannot set them. They're shown here so you can see how the protocol implementation is driven. As a merchant, you control per-proxy pricing, endpoint pricing rules, and token validity through the dashboard or the proxy management REST API. (If you self-host L402 via the native middleware, your own app uses equivalent settings.)
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 (initial challenge)β
{
"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": "2026-07-03T13:00:00Z"
}
}
Failed verification β a fresh 402, never 401/403β
When a presented L402 credential fails verification β malformed credential, preimage/payment-hash mismatch, invalid macaroon signature, expired token, or a caveat violation (wrong path, merchant, or price tier) β Lightning Enable does not return 401 Unauthorized or 403 Forbidden. It re-issues a fresh 402 challenge with a new invoice and macaroon. The failure reason is carried in:
- the
X-L402-Errorresponse header, and - the body's
messagefield.
HTTP/1.1 402 Payment Required
WWW-Authenticate: L402 macaroon="AgEL...", invoice="lnbc100n1p3..."
X-L402-Error: Macaroon has expired
{
"error": "Payment Required",
"message": "Macaroon has expired",
"l402": {
"macaroon": "AgEL... (new macaroon)",
"invoice": "lnbc100n1p3... (new invoice)",
"amount_sats": 100,
"payment_hash": "def456...",
"expires_at": "2026-07-03T13:10:00Z"
}
}
Clients should treat every 402 as a (re-)challenge: check X-L402-Error to learn why the previous credential was rejected, then pay the new invoice from the current response. Code that branches on 401/403 for L402 failures will never execute those branches.
Next Stepsβ
- API Monetization - Protect your endpoints
- Proxy Configuration - Monetize any API
- API Reference - Complete L402 API docs