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│ │ 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:

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 OpenNode charge ID 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, 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_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

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