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.