Skip to content

docs: add L402 discovery specification#27

Open
Roasbeef wants to merge 1 commit into
masterfrom
claude/dazzling-chaplygin-4fac81
Open

docs: add L402 discovery specification#27
Roasbeef wants to merge 1 commit into
masterfrom
claude/dazzling-chaplygin-4fac81

Conversation

@Roasbeef

@Roasbeef Roasbeef commented Jun 9, 2026

Copy link
Copy Markdown
Member

In this PR, we add a discovery layer for L402. Today the protocol is reactive:
you only find out what a service offers, or how it prices things, by hitting a
resource and getting a 402 back with a single invoice for a single thing.
There's no way to look at the catalog, the prices, the tier structure, or even
the caveat vocabulary a server enforces before you commit to paying. We close
that gap, and in doing so give L402 a natural home for dynamic pricing and the
start of in-protocol negotiation.

Discovery is built from two pieces that share one vocabulary. The first is a
static manifest, served free at /.well-known/l402.json
(RFC 8615), that describes a provider's
services, capabilities, tiers, constraints, prices, and the caveat conditions
its satisfiers actually understand. The second is an optional quote endpoint:
the client proposes a bundle (a service, a tier, some capabilities, some
constraint values) and the provider answers with a price and a ready-to-pay
challenge, plus counter-offers. The manifest comes in two interchangeable
profiles, a compact bespoke JSON document and a fully self-describing
OpenAPI 3.1 document that carries the same pricing
inline through an x-l402-* extension vocabulary, so a single fetch can teach a
client the whole API including its prices.

No new verification machinery

The property that makes this fit cleanly is that discovery doesn't add a second
payment or verification path. A quote response is just an ordinary L402
challenge, a macaroon committing to a payment hash alongside an invoice, except
it's minted proactively for a bundle the client chose rather than for a fixed
resource it happened to request. Once the client holds it, the rest is the base
protocol unchanged: pay the invoice, present
Authorization: L402 <macaroon>:<preimage>, done. Discovery only changes how
the challenge gets selected, never how it's honored.

To make that reuse precise rather than hand-wavy, the spec pins down the
bundle-to-caveat mapping (how {service, tier, capabilities, constraints}
becomes services=s:t, s_capabilities=..., and one key=value per
constraint), and requires the client to verify the returned macaroon binds to
the invoice and encodes the bundle it asked for before paying. We also tie the
quote expiry to the BOLT 11 invoice's own expiry, since the credential carries
no notion of a quote price or its expiry: the only way an expired quote actually
becomes unpayable is for the underlying invoice to expire with it.

What's in the spec

The bulk of the change is discovery-spec.md, written in the same RFC style as
the existing protocol spec. It covers the manifest format, the quote endpoint
with a structured error model and budget-aware negotiation, the OpenAPI profile
and its x-l402-* vocabulary, and the security considerations w.r.t
authenticity, price integrity, and quote-endpoint DoS. agent-spec.md gains a
token-efficient discovery section that covers the same ground for autonomous
clients.

Discovery is fully optional and backwards compatible. A provider that implements
none of it stays L402-compliant, and a discovery-unaware client just falls back
to the reactive 402 flow.

BOLT 12 is left in as experimental: a macaroon has to commit to the payment
hash, but a BOLT 12 offer doesn't hand you one until you fetch an invoice over
an onion message, so the deferred-binding flow needs its own treatment in a
later revision.

A few gaps are deliberately left open for a follow-up: client-side privacy of
token_id correlation, root-key storage growth from unpaid quotes, an
idempotency key for quote retries, per-resource fiat pricing, and version-parsing
rules.

The base L402 protocol is reactive: a client learns what a service offers,
and what it costs, only by hitting a resource and getting a 402 back with a
single invoice for a single thing. There is no way to see the catalog, the
prices, the tier structure, or the caveat vocabulary ahead of time. In this
commit, we add an optional discovery layer that closes that gap and gives the
protocol a place for dynamic pricing and the start of negotiation.

Discovery is built from two pieces that share one vocabulary. The first is a
static manifest, served free at /.well-known/l402.json, that describes a
provider's services, capabilities, tiers, constraints, prices, and the caveat
conditions its satisfiers enforce. The second is an optional quote endpoint:
the client proposes a bundle, and the provider answers with a price and a
ready-to-pay challenge, plus counter-offers. The manifest comes in two
interchangeable profiles, a compact bespoke JSON document and a fully
self-describing OpenAPI 3.1 document that carries the same pricing inline
through an x-l402-* extension vocabulary, so a single fetch can teach a client
the whole API including its prices.

The load-bearing property is that discovery adds no new payment or
verification machinery. A quote response is an ordinary L402 challenge, a
macaroon committing to a payment hash alongside an invoice, minted proactively
for a bundle the client chose rather than for a fixed resource it happened to
request. To make that reuse precise, the spec pins down the bundle-to-caveat
mapping, requires the client to verify the returned macaroon binds to the
invoice and encodes the requested bundle before paying, and ties the quote
expiry to the BOLT 11 invoice expiry, since the credential itself carries no
notion of a quote price or its expiry. It also defines a structured error
model for the quote endpoint, a budget-aware negotiation request, and the
security considerations for authenticity, price integrity, and quote-endpoint
denial of service.

The agent specification gains a token-efficient discovery section covering the
same ground for autonomous clients. Discovery is fully optional and backwards
compatible: a provider that implements none of it remains L402-compliant, and
a discovery-unaware client falls back to the reactive 402 flow unchanged.
@DrVelvetFog

Copy link
Copy Markdown

Coming over from the x402 side — rolznz's question in #22 is how I found this. Context so the comparisons below have some grounding: I operate an x402 facilitator (Sui) and wrote the spec text for x402's settlement-receipt-binding extension (x402-foundation/x402#2666, still in review), so discovery and "what can you prove you paid for" are the two surfaces I've spent the most time on over there. Read the spec end to end; four things, roughly in order of consequence.

1. The aggregator case is where this will actually live, which makes the OPTIONAL signature load-bearing.

x402's bazaar extension went the opposite way structurally: discovery metadata rides on the 402 response itself and third-party facilitators index it into catalogs — there's no first-party well-known manifest. The practical consequence is that most of that spec's security section ended up being about catalog poisoning at the indexer, because clients overwhelmingly meet resources through an index, not the origin. §10.1 here names the same deployment ("an aggregator or marketplace that indexes many providers") and defers the signature and its canonicalization to a future revision. Based on how it played out on our side, indexed manifests will be the common path for agent clients, not the edge case — so two requests for that future revision:

  • Pin canonicalization to RFC 8785 (JCS) explicitly rather than "the canonical serialization." We've now had two independent implementations produce different hashes over the same document because one used sorted-keys JSON serialization, which agrees with JCS on all-string documents and silently diverges on numeric literals (1.0 vs 1). It's the kind of divergence that survives every test until a real document hits it.
  • §10.1 already contemplates signing quotes as well as manifests; one signature spec covering both shapes would spare implementers doing the exercise twice.

2. The design already yields an offline-verifiable proof of purchase — worth one informative paragraph making that explicit.

This is the question I came in with, since it's the thing we had to bolt on in x402: after the fact, what can a client prove about what it bought? Here the answer falls out of existing structure: the BOLT 11 invoice is signed by the provider's node and carries the amount; the macaroon identifier commits to that invoice's payment hash; the preimage proves settlement; and §5.7's deterministic bundle-to-caveat mapping makes the purchased bundle legible from the macaroon's own caveats. So (invoice, preimage, macaroon) is a self-contained receipt a third party can verify against the manifest's node_pubkey with no provider cooperation — "paid amount A to node N for bundle B." §6.2's pre-payment checks are exactly this verification run client-side. On the x402 side nothing in the base flow was independently recomputable, so it needed its own extension; L402 gets it nearly free, and a short informative section stating the property — and its limits: it proves purchase, not delivery, and not the quote history — would let agent tooling rely on it deliberately rather than by accident. Happy to draft that paragraph if you'd take it.

3. Quote-endpoint cost: let the client signal intent on the wire.

§10.3's price-only quote ("minting the challenge only when the client signals intent to pay") is the right escape valve for the mint-per-quote cost — unauthenticated requests that force priced work server-side is a griefing class I have to think about on the facilitator verify path too. But as written there's no wire field for that intent, so a provider has to guess from heuristics. A commit (or intent) boolean on the quote request would make price-only vs. full-challenge deterministic, directly cut the root-key storage growth from unpaid quotes (already on your open-gaps list), and give the planned idempotency key a natural home: exploratory requests don't need one, committed ones do.

4. Small one on §6.2, check 2.

The client must confirm the caveats are "exactly the bundle-to-caveat mapping" of the requested bundle. Does "exactly" intend to preclude provider-internal bookkeeping caveats entirely (ops tags, deployment metadata)? §5.7 forbids widening and forbids omitting requested constraints, but a strict exact-match check also fails closed on any benign extra narrowing caveat. That's probably the right call — a client can't distinguish benign from not — but if it's deliberate it may deserve one explicit sentence, so an implementer doesn't "helpfully" relax the check later.

None of this touches the core shape, which reads ready to implement against. And the manifest publishing the caveat vocabulary with attenuation rules (§5.5) has no x402 analog — for agent clients that want to sub-delegate credentials, that's quietly one of the most useful parts of the document.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants