Capability Manifest

The l402_manifest directive turns a location into a discovery endpoint that emits a JSON description of every L402-protected route on the server. It is intended to live at /.well-known/l402-services (RFC 8615), making this instance self-describing to clients that have only the host.

location = /.well-known/l402-services {
    l402_manifest;

    # Optional: restrict who can scrape pricing details.
    # allow 10.0.0.0/8;
    # deny  all;
}

This is the agent-era equivalent of robots.txt or security.txt. An autonomous agent (or any client) given only https://example.com can fetch the manifest, learn which routes are paid, how much they cost, and which payment backends are accepted — without any out-of-band integration.


Example response

{
  "version": "1",
  "service": {
    "name": "Example API",
    "description": "Stock data API"
  },
  "payment_methods": [
    {
      "type": "lightning",
      "backend": "LNURL",
      "address": "hello@getalby.com"
    },
    {
      "type": "cashu",
      "mints": ["https://mint.minibits.cash"],
      "p2pk_supported": true,
      "challenge_header": "X-Cashu"
    }
  ],
  "routes": [
    {
      "path": "/protected",
      "price": {
        "type": "static",
        "amount_msat": 10000
      },
      "caveats_required": ["RequestPath = /protected"]
    },
    {
      "path": "/rate-limited",
      "price": {
        "type": "static",
        "amount_msat": 10000
      },
      "caveats_required": ["RequestPath = /rate-limited"],
      "rate_limit": {
        "max_requests": 2,
        "window_secs": 60
      }
    }
  ]
}

What the manifest describes

Field Source Meaning
version constant "1" Schema version. Bumped on breaking changes; agents should reject unknown majors.
service.name, service.description, service.operator, service.contact env vars L402_SERVICE_NAME, L402_SERVICE_DESCRIPTION, L402_SERVICE_OPERATOR, L402_SERVICE_CONTACT Optional, omitted when unset.
payment_methods[].type lightning or cashu Which payment rail this method describes.
payment_methods[].backend env var LN_CLIENT_TYPE LNURL, LND, CLN, NWC, BOLT12, ECLAIR, LNC.
payment_methods[].address env var LNURL_ADDRESS (LNURL backends only) Server-default LN address. May be overridden per-route via lnurl_addr.
payment_methods[].mints env var CASHU_WHITELISTED_MINTS Allowed Cashu mints (when Cashu is enabled).
payment_methods[].p2pk_supported env var CASHU_P2PK_MODE Whether NUT-24 P2PK Cashu is enabled.
routes[].path location directive URL path served by this route.
routes[].price.amount_msat l402_amount_msat_default Base price after merge_loc_conf.
routes[].caveats_required derived Caveats the issued macaroon will carry. Today always [RequestPath = <path>].
routes[].macaroon_timeout_secs l402_macaroon_timeout Omitted when 0 (no expiry).
routes[].lnurl_addr l402_lnurl_addr Per-route LNURL override for multi-tenant deployments.
routes[].rate_limit l402_invoice_rate_limit Server-side invoice rate limit applied before challenge issuance.
routes[].auto_detect_payment l402_auto_detect_payment When true, clients can omit the preimage and the server settles via node lookup.

Dynamic (Redis-backed) pricing is not reflected in price.amount_msat — the manifest emits the static default. Dynamic prices change per request and would require a Redis round-trip per route to render accurately; that's out of scope for v1.


Hiding a route

Operators may want certain paid routes to remain undiscoverable — private APIs, beta tiers, customer-specific endpoints. Use l402_manifest_hide; on the location:

location /internal-paid {
    l402 on;
    l402_amount_msat_default 100000;
    l402_manifest_hide;       # not advertised in /.well-known/l402-services
}

The route still enforces L402 normally. It just doesn't appear in the manifest's routes[] array.


Service-level metadata

The optional service block is read from environment variables at manifest-render time:

L402_SERVICE_NAME="Example API"
L402_SERVICE_DESCRIPTION="Premium financial data, paid per request."
L402_SERVICE_OPERATOR="npub1abcd..."   # Nostr pubkey, DID, or free-form
L402_SERVICE_CONTACT="ops@example.com"

All four are optional. Unset variables are omitted from the response so the manifest stays valid JSON even with no service metadata.


Caveats and limitations

  • Per-worker registry. The manifest registry is per-nginx-worker. On a multi-worker deployment, every worker sees the same routes (config is shared), so this is a non-issue for the manifest itself. (Unlike the l402_metrics counters, which use a shared-memory zone, the registry is read-only per-worker state and needs no cross-worker aggregation.)
  • No authentication by default. Pricing information is public. Restrict the endpoint with allow/deny, an auth subrequest, or a firewall if competitors should not see your full pricing matrix.
  • Reload behaviour. On nginx -s reload, new workers start with a fresh registry built from the new config. Old workers serve in-flight requests with their existing registry until they exit.

Why this matters

Without a manifest, every L402 integration is a bespoke wiring job: the client must be told the routes, prices, payment backends, and caveat formats out of band. With one, an agent can land on a host and onboard itself end-to-end:

GET /.well-known/l402-services  → learn the API surface
GET /protected                → receive 402 + bolt11 invoice
PAY the invoice               → get preimage
GET /protected with L402 auth → success

For autonomous agents — Claude tools, MCP servers, custom-built — this is the difference between L402 being a protocol and L402 being a discoverable web standard.