Skip to main content

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:

CaveatPurpose
pathBinds the token to the API path it was issued for. Supports exact match or wildcard prefix (e.g., /l402/proxy/my-api/*).
merchant_idBinds the token to the issuing merchant, preventing cross-tenant token reuse.
amount_satsBinds the token to the price at issuance, preventing reuse at a different price tier.
expiresSets the token expiration as a Unix timestamp (default: 1 hour).
charge_idRecords the payment provider's charge ID (Strike or OpenNode) for the associated payment.
servicesIdentifies 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:

  1. Client pays once
  2. Receives preimage
  3. Uses same macaroon:preimage for subsequent requests
  4. Token valid until expiration β€” configurable per proxy in the dashboard wizard, falling back to the global 1-hour default
note

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_KEY environment 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​

Internal server configuration β€” shown for protocol understanding

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-Error response header, and
  • the body's message field.
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​