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

  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

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