Skip to content

Migrating from L402

Already running an L402-gated API (Aperture or similar)? s402 is the first 402 protocol that reads L402 natively. Your Lightning-paying clients keep working; your s402 clients get six schemes Lightning structurally can't express.

This guide is for teams running Aperture-style Lightning paywalls who want to extend — not replace — their setup.

Availability

L402 read-path lands in v0.7 — the code is in the s402/compat/l402 entry point. Write-path emission (s402 server emitting an L402 challenge with an issued macaroon + fresh invoice) is not scoped: it requires a running Lightning node to mint invoices, which is out of scope for a wire-format library. Teams that want to emit L402 should keep using Aperture.

TL;DR

  • s402 will read L402 / LSAT challenges via s402/compat/l402 (shipping v0.7)
  • Your existing Aperture deployment keeps working
  • You gain six schemes Lightning structurally cannot express: Upto ceiling, Escrow with arbiter, Stream with on-chain rate cap, Unlock pay-to-decrypt, Prepaid batched settlement, Exact on any chain beyond Bitcoin
  • Coexistence via Accept-Payment: advertise both L402 and s402 on the same endpoint

When to pick s402 over L402

SituationRight tool
You need sub-millisecond payment finalitys402 Prepaid on Sui — ~400ms vs Lightning's multi-hop routing (variable, seconds)
Your API has variable pricing and you must bound the maximum charges402 Upto — the ceiling is enforced by a Move contract
You're running per-second billing (inference, video, live data)s402 Stream — on-chain rate enforcement
Trustless commerce between unfamiliar parties with arbiter-backed disputess402 Escrow
Pay-to-decrypt contents402 Unlock — via Sui SEAL + Walrus
You don't want to run a Lightning nodes402 — chain-agnostic, hosted facilitator available

When to stay on L402

Lightning has properties s402 doesn't replicate:

  • Bitcoin-native settlement — if your users prefer Bitcoin and your API ingests sats, Lightning is the path of least resistance
  • Existing Lightning wallet ecosystem — Phoenix, Wallet of Satoshi, Breez, Zeus — huge installed base
  • Privacy via onion routing — Lightning's source-route topology is stronger than most on-chain systems
  • You already run Aperture — don't migrate for migration's sake

Consuming an L402 challenge as an s402 client

This is the primary v0.7 capability — an s402 client receives a 402 from an Aperture server, lifts the challenge into s402 types, and routes it to the caller's Lightning wallet.

typescript
import {
  parseWwwAuthenticateL402,
  fromL402Challenge,
} from 's402/compat/l402';

const res = await fetch('https://api.example.com/data');
if (res.status === 402) {
  const challenge = parseWwwAuthenticateL402(
    res.headers.get('WWW-Authenticate'),
  );
  if (challenge) {
    const requirements = fromL402Challenge(challenge);
    // requirements.network  === 'lightning:mainnet'
    // requirements.asset    === 'lightning:msat'
    // requirements.amount   === '250000000'            (for a 2500 μBTC invoice)
    // requirements.payTo    === 'lightning:invoice'    (sentinel — real destination is in the invoice)
    // requirements.extensions.l402.macaroon           (present back on retry)
    // requirements.extensions.l402.invoice            (pay this via a Lightning wallet)

    // Your Lightning wallet pays the invoice and returns the preimage.
    const preimage = await yourLightningWallet.pay(requirements.extensions.l402.invoice);

    // Retry with L402 Authorization.
    await fetch('https://api.example.com/data', {
      headers: {
        Authorization: `L402 ${requirements.extensions.l402.macaroon}:${preimage}`,
      },
    });
  }
}

parseWwwAuthenticateL402 accepts both the modern L402 and the legacy LSAT auth-scheme names — Aperture still emits LSAT on older deployments. The parsed output is always canonicalized to L402.

BOLT-11 HRP decoding

fromL402Challenge calls decodeBolt11Summary internally to extract the amount and network from the invoice's human-readable part. This is a partial BOLT-11 decoder — it reads only the HRP (prefix + amount + multiplier) because full tagged-field parsing requires 500+ lines of bech32 code that the Lightning wallet already has.

BOLT-11 multiplierConversion to msat
(none)amount * 10^11
m (milli-BTC)amount * 10^8
u (micro-BTC)amount * 10^5
n (nano-BTC)amount * 10^2
p (pico-BTC)amount / 10 (amount must be multiple of 10)

What's not in the read path yet

  • Macaroon caveat decoding — the macaroon is passed through opaque; we do not decode or enforce caveats (expiry, capability bindings). Clients that need caveat introspection should use node-macaroon or equivalent.
  • Preimage verification — server-side, requires Lightning node access. Out of scope for a wire-format library.
  • Invoice tagged-field parsing — node pubkey, routing hints, payment hash, description — all inside the invoice but left for the Lightning wallet that actually pays.
  • BOLT-12 offers — the newer offer-based protocol is not yet supported; spec is still evolving.

Coexistence pattern via Accept-Payment

Advertise both L402 and s402 on the same endpoint; each client pays its native way.

typescript
import { parseAcceptPayment, selectBestScheme, S402_HEADERS } from 's402';

async function handle(req: Request): Promise<Response> {
  const preferred = parseAcceptPayment(req.headers.get(S402_HEADERS.ACCEPT_PAYMENT));

  const supported = [
    's402/prepaid',     // s402 high-frequency native
    's402/exact',       // s402 one-shot
    'l402/lightning',   // L402 Lightning (advertised but emitted by your Aperture path)
  ];

  const chosen = selectBestScheme(preferred, supported);

  if (chosen?.startsWith('l402/')) {
    return routeToApertureHandler(req);   // your existing L402 path
  }

  return buildS402Challenge(chosen ?? 's402/exact');
}

L402 clients pay via Lightning (routed to Aperture). s402 clients pay via s402 (Sui, EVM, etc.). Neither client stack changes.

Honest comparison

DimensionL402s402
Schemes1 (Lightning invoice + macaroon)6 (Exact, Upto, Prepaid, Escrow, Stream, Unlock)
SettlementLightning Network (Bitcoin)Chain-agnostic (Sui native; EVM + Solana via schemes)
FinalitySeconds (multi-hop routing)~400ms on Sui
EnforcementMacaroon caveats (server-signed)Move contracts (on-chain)
Node requirementMust run Lightning nodeHosted facilitator available
Multi-chainBitcoin onlyAny chain with an s402 adapter
Pricing modelPer-call (one invoice per request)Per-call + batch + streaming + unlock

L402 wins on Bitcoin-native settlement and a mature Lightning wallet ecosystem. s402 wins on expressiveness, finality, enforcement model, and multi-chain reach.

FAQ

Do I need to migrate off Aperture?

No. The coexistence pattern is first-class — advertise both and let clients pick.

Does s402 emit L402 challenges?

No. L402 emission requires minting BOLT-11 invoices, which requires a Lightning node. That's Aperture's job. If you need to emit L402, keep Aperture in the path.

Can I reuse my Aperture macaroon infrastructure with s402?

Not directly — macaroons are L402-specific. s402 uses signed receipt claims (NFT receipts on Sui, or HTTP header receipts for lightweight flows). The two are conceptually similar (bearer tokens with expiry + caveats) but structurally incompatible on the wire.

What happens to the payTo field since Lightning invoices don't expose a traditional address?

payTo is set to the sentinel "lightning:invoice". The real destination (node pubkey + payment hash) is inside the BOLT-11 invoice, which the Lightning wallet decodes itself. The sentinel exists only to satisfy the s402 schema.

Why is asset set to "lightning:msat" instead of a token identifier?

Lightning payments are denominated in millisatoshi — they don't carry a token identifier. The lightning:msat string is an s402 convention that signals "this amount is in millisatoshi, pay via Lightning Network."

Next steps

If you're running Aperture and want to pilot s402 alongside it, file an issue — we'll help you wire up the coexistence pattern.

Released under the Apache 2.0 License.