L402
L402 is an open authentication and metering protocol from Lightning Labs (originally called LSAT). It piggybacks on the HTTP 402 Payment Required status code, but the payment leg runs on the Bitcoin Lightning Network and authentication is carried by macaroons — bearer tokens with embedded caveats.
The wire flow:
- The resource server returns
402 Payment Requiredwith aWWW-Authenticate: L402 macaroon="...", invoice="lnbc..."header. The macaroon’s identifier commits to the invoice’s payment hash; the invoice is a BOLT 11 Lightning invoice. - The client pays the Lightning invoice through their LN node and receives the 256-bit preimage as proof of payment.
- The client retries with
Authorization: L402 <macaroon>:<preimage>. The server verifies thatSHA-256(preimage)equals the macaroon-committedpayment_hash, then returns the resource.
L402 is zero-gatekeeper — any operator with reachable Lightning routing can issue and accept L402 challenges. No foundation approval, no merchant network onboarding, no required facilitator.
Keel supports L402 at the evidence rung today:
- Evidence: L402 proof-of-payment evidence binds into a signed permit at
resource_attributes_json.l402for tamper-evident audit and policy adjudication. - Runtime: not shipped. The Keel-driven LN-payment runtime is the highest-leverage demand-gated runtime on the roadmap (no partner approval required), but it ships only when customer demand justifies operating production Lightning channels.
Use this guide when your application wants to:
- Bind a self-executed L402 payment into Keel for decision authority and audit, OR
- Adjudicate spend policy against L402-priced resources without paying directly through Keel.
Keel does not custody Lightning Network channels or payment-route private keys today. The evidence path is the supported integration. Lightning runtime adoption tracks customer signal; reach out if you want it prioritised.
Architecture
Customer app / agent
|
| 1. GET <target URL> on the L402-protected resource server
v
Resource server
|
| 2. Return 402 Payment Required with
| WWW-Authenticate: L402 macaroon="...", invoice="lnbc..."
v
Customer app
|
| 3. Parse the macaroon identifier; pay the BOLT 11 invoice
| through the customer's LN node; receive the 256-bit preimage
| 4. Retry: GET <target URL> with
| Authorization: L402 <macaroon>:<preimage>
v
Resource server
|
| 5. Verify and return the resource
v
Customer app
|
| 6. POST /v1/permits with action.name=l402.access and
| resource.attributes.l402 = the 4-field L402Evidence
v
Keel
|
| 7. Validate: SHA-256(payment_preimage_hex) == payment_hash_hex
| 8. Decide against policy
| 9. Bind L402 evidence into the signed permit;
| substrate-v6 binding hash recursively covers it —
| tamper-evident on verify
v
Customer appThe trust domains stay separate:
| Domain | Owner | Keel role |
|---|---|---|
| Lightning Network channel & node keys | Customer’s LN node (LND, CLN, Eclair, or a custodial provider) | Never sees the LN node key. The preimage is the only payment artifact Keel records, and the preimage is a settlement proof — knowing it does not let Keel spend funds. |
| Spend decision | Keel permit policy | Decide whether this agent may access this L402-priced resource. |
| Settlement rail | Lightning Network (BOLT 11 invoice, off-chain HTLC routing) | Record rail outcome and bind the proof-of-payment evidence. |
| Audit trail | Keel + the Lightning Network ledger (operator-side, not globally public) | Preserve tamper-evident audit evidence; cross-check against operator-side LN records on disputes. |
Prerequisites
- An L402-protected resource server endpoint (Aperture-fronted services, Lightning Labs APIs, or any operator running an L402 challenger). For development you can host a mock challenger or use a public test endpoint.
- A Lightning Network node with outbound liquidity in satoshis. Common setups: LND, Core Lightning (CLN), or a custodial provider exposing a
payinvoiceAPI. - A Keel project on Production or Enterprise. See Plans & Entitlements.
- A client-scoped Keel API key for
POST /v1/permits.
Set your Keel environment:
export KEEL_BASE_URL="https://api.keelapi.com"
export KEEL_API_KEY="keel_sk_your_project_key"L402 does not require any wallet credentials inside Keel — the LN payment is executed by your app and only the 4-field proof is bound.
Quick start
The example below shows the supported evidence path: your app or agent executes the L402 payment, then issues a Keel permit binding the captured evidence.
Python
pip install keel-sdk httpx
# Plus your LN client of choice (e.g. lnd-grpc-client, pylightning, or a custodial SDK).import base64
import hashlib
import httpx
import uuid
KEEL_BASE = "https://api.keelapi.com"
KEEL_API_KEY = "keel_sk_..." # your project API key
PROJECT_ID = "proj_..." # your Keel project id
AGENT_ID = "agent_l402_buyer"
target_url = "https://merchant.example/l402/report.pdf"
# 1. Probe the resource server for the L402 challenge.
challenge_response = httpx.get(target_url)
assert challenge_response.status_code == 402
www_auth = challenge_response.headers["WWW-Authenticate"]
macaroon_b64, invoice = parse_l402_www_authenticate(www_auth)
# Implementations of `parse_l402_www_authenticate` are short — see the L402
# spec, or use a community library such as `aiolsat` / `lsat-python`.
# The macaroon's *identifier* is what Keel records; extract it from the
# macaroon's serialized form per your library.
macaroon_identifier_b64 = extract_identifier_b64(macaroon_b64)
# 2. Pay the BOLT 11 invoice through your LN node.
# The contract is that you receive the 32-byte preimage on settlement.
ln_payment = lightning_client.pay_invoice(invoice) # your LN client
preimage_hex = ln_payment.preimage # 64-character hex string
payment_hash_hex = hashlib.sha256(
bytes.fromhex(preimage_hex)
).hexdigest()
# 3. Retry the resource with the L402 Authorization header.
authorization = f"L402 {macaroon_b64}:{preimage_hex}"
paid_response = httpx.get(target_url, headers={"Authorization": authorization})
paid_response.raise_for_status()
# 4. Issue a Keel permit binding the L402 evidence.
permit_body = {
"project_id": PROJECT_ID,
"idempotency_key": f"l402_{uuid.uuid4().hex}",
"subject": {"type": "agent", "id": AGENT_ID, "attributes": {}},
"action": {"name": "l402.access", "attributes": {"rail_class": "l402"}},
"resource": {
"type": "request",
"id": f"req_l402_{uuid.uuid4().hex}",
"attributes": {
"provider": "l402",
"model": "l402.lightning_labs.v1",
"operation": "cost_permit.authorize",
"modality": "payment",
"execution_mode": "sync",
"estimated_input_tokens": 0,
"estimated_output_tokens": 0,
"max_output_tokens_requested": 0,
"inputs": [],
"l402": {
"protocol": "l402",
"macaroon_identifier_b64": macaroon_identifier_b64,
"payment_preimage_hex": preimage_hex,
"payment_hash_hex": payment_hash_hex,
},
},
},
"context": {"ip": "203.0.113.10", "user_agent": "example-agent/1.0"},
}
resp = httpx.post(
f"{KEEL_BASE}/v1/permits",
headers={"Authorization": f"Bearer {KEEL_API_KEY}"},
json=permit_body,
)
resp.raise_for_status()
permit = resp.json()
print(permit["permit_id"]) # signed v6 permit binding the L402 evidence
print(permit["binding_version"]) # "v6"Keel validates that SHA-256(payment_preimage_hex) == payment_hash_hex at issue time. A mismatch returns a 422 with a Pydantic validation error — the preimage was either fabricated or for a different invoice.
Request shape
L402 uses the standard POST /v1/permits envelope. The rail evidence rides at resource.attributes.l402.
{
"project_id": "proj_xxxxxxxxxxxxxxxx",
"idempotency_key": "l402_a1b2c3d4...",
"subject": {
"type": "agent",
"id": "agent_l402_buyer",
"attributes": {}
},
"action": {
"name": "l402.access",
"attributes": { "rail_class": "l402" }
},
"resource": {
"type": "request",
"id": "req_l402_e5f6...",
"attributes": {
"provider": "l402",
"model": "l402.lightning_labs.v1",
"operation": "cost_permit.authorize",
"modality": "payment",
"execution_mode": "sync",
"estimated_input_tokens": 0,
"estimated_output_tokens": 0,
"max_output_tokens_requested": 0,
"inputs": [],
"l402": {
"protocol": "l402",
"macaroon_identifier_b64": "dmVyc2lvbj0wO3VzZXJfaWQ9ZmVkNzRiM2VmM...",
"payment_preimage_hex": "79852a0791225dee00be0a6cf31a1619782c21d35995e118bfc74ad812174035",
"payment_hash_hex": "5b8f3c0a9b1e2d4f6a7c8e9b0d2f1e3c5a4b7e0d8f1c2a3b4e5d6f7c8a9b0c1d"
}
}
},
"context": {
"ip": "203.0.113.10",
"user_agent": "example-agent/1.0"
}
}The payment_hash_hex shown is illustrative — at the API the verifier checks SHA-256(payment_preimage_hex) == payment_hash_hex and rejects mismatches.
L402Evidence reference
L402Evidence is intentionally minimal — exactly the four fields required to prove an L402 payment off the captured macaroon identifier and preimage. The Pydantic model is extra="forbid", so any field not in this table is rejected with a 422 at issue time.
| Field | Type | Description |
|---|---|---|
protocol | string | Always "l402". |
macaroon_identifier_b64 | string (base64) | Base64 encoding of the raw L402 macaroon identifier bytes. The identifier commits to the Lightning invoice’s payment hash. |
payment_preimage_hex | string (64-char hex) | Hex-encoded 32-byte Lightning payment preimage — proof that the BOLT 11 invoice was paid. |
payment_hash_hex | string (64-char hex) | Hex-encoded 32-byte payment hash committed by the L402 macaroon identifier. Must equal SHA-256(payment_preimage_hex); this is checked at issue time. |
The preimage is the proof-of-payment artifact. Possession of a valid preimage whose SHA-256 matches a paid invoice’s payment hash is what authenticates the request — no key material, no signature. This is a property of the Lightning Network HTLC mechanism, not Keel.
Why isn’t the invoice or amount stored in the evidence? Because the macaroon identifier already commits to the payment hash, and the payment hash already commits (via the BOLT 11 invoice metadata) to the amount. Storing the invoice or the amount inside the evidence would be redundant data that the substrate-v6 binding hash would then have to cover — the four-field representation is the cryptographically minimal proof.
The full JSON Schema is published in openapi.json under L402Evidence.
Amount tracking and policy
L402 amount enforcement does not happen via a per-rail spend authority. The flow is:
- The BOLT 11 invoice’s
amount_msatis committed by itspayment_hash. - The
payment_hashis committed by the macaroon identifier. - The macaroon identifier is in the evidence.
So the amount is indirectly committed through the macaroon → invoice chain. Keel’s policy substrate adjudicates the access decision against your project’s policies; if you need amount-level policy, capture the invoice amount in your application before paying and pass it through your decision context (the subject.attributes or context fields are appropriate carriers — see Policies).
If amount-level policy on L402 specifically matters for your use case, file a feature request. The first-class rail-side spend-authority surface that Stripe MPP and x402 expose is roadmap-gated for L402.
Response shape
POST /v1/permits returns a PermitResponse with the decision and policy metadata. The binding hashes and signatures live on the permit receipt, fetched separately at GET /v1/permits/{permit_id}/receipt. This split keeps the issue response small and lets you defer the larger cryptographic surface to the moment you actually need it (verification, export, dispute).
Issue response (POST /v1/permits)
{
"permit_id": "01HXYZ...",
"project_id": "01HXYZ...",
"decision": "allow",
"reason": "policy_evaluated:l402.access:allow",
"display_decision": "allow",
"reason_code": "l402_access_allowed",
"display_reason": "Allowed.",
"decision_source": "policy_engine",
"metadata": {
"policy_id": "pol_default",
"policy_version": "2026-06-09T17:42:01Z",
"evaluated_at": "2026-06-09T17:42:01Z",
"action": { "name": "l402.access" }
},
"actions": []
}decisionis the policy outcome:"allow","deny", or"challenge".display_decisionmay expand to include"throttle"for presentation purposes.requires_counter_signature,requires_operator_approval, andrequires_distinct_approverflag any pre-execution gates the policy attached.
The exact field set is PermitResponse — see Permits for the full surface.
Receipt response (GET /v1/permits/{permit_id}/receipt)
The receipt carries the substrate-v6 binding fields:
request_hashes.resource_attributes_canonical_hash— RFC 8785 canonical SHA-256 ofresource_attributes_json(including the L402 evidence).request_hashes.binding_canonical_hash— the top-level binding hash that recursively covers the resource attributes hash.signatures.binding_version—"v6"on substrate-v6 permits.signatures.binding_signature— the Ed25519 signature over the binding hash.signatures.binding_key_id— the signing key identifier; resolves to a public key in the verifier’s trust root.
keel-verify consumes this receipt structure directly (via the signed-export bundle). You can also pull the receipt over HTTP for ad-hoc inspection.
Error handling
| Scenario | Behavior |
|---|---|
| Resource server does not return 402 | Keel does not infer payment is required. Your app must initiate the L402 flow explicitly. |
WWW-Authenticate is malformed | Your client raises before contacting Keel. The substrate has no opinion on malformed challenges; record the failure in your application logs. |
payment_preimage_hex or payment_hash_hex doesn’t match the ^[0-9a-f]{64}$ pattern | 422 Pydantic validation error — the fields must be exactly 64 lowercase hex characters. |
SHA-256(payment_preimage_hex) != payment_hash_hex | 422 with payment_hash_hex must equal sha256(payment_preimage_hex). The preimage was either fabricated or for a different invoice. |
macaroon_identifier_b64 is not valid base64 | 422 with macaroon_identifier_b64 must be valid base64. |
An unknown field is added to l402 (e.g. invoice, amount_msat) | 422 — the L402Evidence model is extra="forbid". Use only the four documented fields. |
| Policy denies the access | decision: "deny". The L402 evidence is still bound (capturing the attempted action). The Lightning payment may have already been made by your app — Keel does not refund; that is the operator’s concern. |
| Policy requires step-up | decision: "challenge". Settle the challenge (counter-signature, additional approval, etc.) before proceeding downstream. |
| Tampered evidence detected on verify | permit.binding.v6.binding_hash_mismatch. Any post-issuance modification to the L402 evidence breaks the v6 binding hash. |
Audit trail
The signed permit binds the L402 evidence into the v6 resource_attributes_canonical_hash. To verify offline:
-
Export the permit through Signed Exports.
-
Install
keel-verifier:pip install keel-verifier keel-verify export <export-file> --json -
The verifier recomputes
resource_attributes_canonical_hashfrom the rawresource_attributes_jsonand compares it to the signed value. Any tampering with the macaroon identifier, preimage, or payment hash is detected aspermit.binding.v6.binding_hash_mismatch.
You can also re-check the preimage / payment-hash binding independently: any RFC-compliant SHA-256 implementation will compute payment_hash_hex from the recorded payment_preimage_hex. This is an integrity check that holds even without the Keel signing key — useful if you need a quick verification pass without keel-verifier installed.
See Independent Verification for the full verification story.
FAQ
Why no runtime rung yet?
L402 runtime requires Keel to operate production Lightning channels with outbound liquidity for customer payments. The engineering work is well-understood, but Lightning channel management is operationally heavy (liquidity, channel rebalancing, on-chain emergencies, regulatory exposure depending on jurisdiction). We ship it when concrete customer demand justifies the operational footprint. The evidence rung covers the audit and decision-authority story today.
Is L402 only for Lightning Labs services?
No. The L402 spec is open and there are independent challenger implementations. Lightning Labs’s Aperture reverse proxy is the most common challenger, but operators can implement L402 directly. Keel’s evidence path is challenger-agnostic — it doesn’t depend on who issued the macaroon, only that the preimage / payment-hash binding holds.
What’s the difference between L402 and the old LSAT?
LSAT (Lightning Service Authentication Token) is the historical name; L402 is the renamed, current spec. The wire format is identical for practical purposes. Some older docs and client libraries still reference LSAT — the protocol field in L402Evidence is always "l402" regardless of which name the source documentation uses.
Why is the schema only four fields?
The four fields are the cryptographic minimum to prove an L402 payment. The macaroon identifier commits to the payment hash; the payment hash commits to the preimage; the preimage proves payment. Anything else (invoice text, amount, node pubkey, settlement timestamps) is either derivable from these four or operationally orthogonal to the proof. Adding extra fields just increases the surface that the substrate-v6 binding hash has to cover — at no cryptographic benefit. If you need to capture additional metadata for your own audit, put it in subject.attributes or context rather than inside l402.
Can I combine L402 with other Keel features?
Yes. L402 permits compose with Budget Envelopes, Parent-Child Verb Delegation, Workflow Intent, and the rest of the Keel substrate. The action verb is l402.access and the operation is cost_permit.authorize, the same cost-permit substrate other payment rails use.
Is L402 evidence tamper-evident even after Keel signs it?
Yes. Substrate-v6 binds the full resource_attributes_json mapping (including resource_attributes_json.l402) into the signed permit binding hash. Any change to any nested field of the L402 evidence is detectable on independent verification. Additionally, the preimage-to-payment-hash relationship can be checked independently with any SHA-256 implementation.
What about macaroon caveats?
Macaroons can carry caveats (rate limits, time bounds, capability restrictions). Today Keel records the macaroon identifier, not the full macaroon with caveats — the identifier is what cryptographically commits to the payment hash. Caveat adjudication happens on the resource-server side at challenge time. If you want Keel to policy-evaluate caveats, file a feature request.
Related pages
- Payment Rails Overview — all nine rails at a glance
- Stripe MPP — fiat rail with both evidence + runtime
- x402 — sibling HTTP-402 rail on USDC / EVM networks (evidence + runtime)
- Independent Verification — verify rail evidence with any RFC 8785 implementation
- Permits — the permit primitive
- L402 spec — protocol reference
- BOLT 11 invoice spec — Lightning invoice format