Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

BNAP: a universal plug for priced and permissioned actions — Draft Specification v0.5.1

2026-10-11 · Dominic

Abstract and status

BNAP is a universal plug for priced and permissioned actions: a small socket every deployment shares, and plug points for everything else. A service can accept any credential scheme, take payment over any rail, speak any carrier, choose its own pricing, storage and key custody, and serve agents built on any model or framework; BNAP does not choose for it. Whatever is plugged in, the socket guarantees four things: an offer the server signed, a credential bound to one offer and, where the scheme allows, one request, an outcome recorded once, and a uniform signed receipt.

Terminology. BNAP (“bee-nap”, Blind-Note Admission Protocol) names the protocol; blind notes are its default scheme, not its only one. Caparra is the Rust reference implementation.

TermMeaning
SocketThe rules every deployment shares
PlugAny swappable block: a scheme, rail, carrier, store, signer, pricer, wallet or agent adapter
SchemeA way to prove payment or a property, such as blind notes, L402, x402 or Privacy Pass
BadgeA label for plug mixes guaranteed to interoperate, such as Core-1
HTTP+BNAP/LNBNAP carried in HTTP, funded over Lightning

Status. Pre-draft, version 0.5.1. A fourth design round set a plug architect against a flexibility skeptic and tested the result against the systems in the plug catalogue. An independent review of v0.5 then found four blockers, all fixed in this revision: one payment could be redeemed through several schemes, the Context was circular for artifacts the server makes, payments through native headers bypassed the socket, and the plugin-safety claim was overstated. The v0.4 Cashu profile is now the bnap:notes scheme, specified in the second tab. Key words follow BCP 14.

Owner decisionChoice
ArchitectureA universal plug: one socket, plugs for everything else
BaselineA badge, not a gate: any mix that keeps the socket rules is BNAP; Core-1 labels the mix guaranteed to interoperate
Non-Bitcoin railsPluggable with guardrails: off by default, own unit, held until final
NameBlind-Note Admission Protocol; blind notes are the default scheme
Earlier decisions keptModest framing; withdrawal is the optional BNAP-W; first deployment AI agents and paid APIs
Next step (11 October)Draft the core socket spec with exact byte layouts and test vectors (third tab)
First gatewayAn LLM inference proxy: per-call, metered pricing in front of an OpenAI-compatible endpoint
Agent-1No x402 on Lightning until its spec is published

What v0.5 changed.

  • The core is no longer a Cashu profile but a scheme-agnostic socket; the profile is now one scheme, bnap:notes, the recommended default.
  • Fourteen plug points cover every block, from credential schemes to AI-agent adapters.
  • L402, MPP, x402, Privacy Pass, Web Bot Auth, OAuth and more plug in as peer schemes or attestations; all but two of the systems tested fit, most through adapters that follow conventions already allowed by the native spec.
  • The server signs its offer set, so no proxy can strip a strong scheme from a BNAP offer, and clients that have seen the manifest refuse to fall back to native headers.
  • Route policies check scheme properties from a registry, not brand names, so a weak scheme can never silently satisfy a strong requirement.
  • One payment admits one action, whichever schemes share it: the socket commits each payment’s rail ID alongside the scheme’s use IDs.
  • Plugs never hold keys or write the use store. A hostile scheme can wrongly admit actions only on routes that enable it; it cannot touch other schemes’ uses, keys or receipts.

Why BNAP exists

Agents, scrapers and spammers act at machine speed, and putting a price or a permission on each action stops the actions that are only worth doing for free:

p > v_{action}

Many schemes now solve part of this: L402, x402, MPP, Cashu, Privacy Pass, Web Bot Auth, API keys. Each has its own headers, replay rules, receipts and wallets, so an operator who wants several must build and secure each one, and an agent must speak them all.

BNAP is the socket they share. It replaces none of them and adds four guarantees on top: signed offers, request binding, once-only outcomes and a uniform receipt. Its default scheme, blind notes, adds what none of the others offer together: prepaid credit, spent offline, unlinkable to the payment that funded it.

Non-goals: proving personhood (attestation schemes may do that), stopping well-funded attackers or volumetric floods, replacing the operator’s abuse classifier, and being a token or payment network.

Design principles

  • Plug anything, check everything. Plugs propose; the socket decides.
  • Properties, not brands. A route requires properties such as single-use, request-bound or unlinkable; any scheme with those properties qualifies.
  • Label, don’t gate. Any mix of plugs that keeps the socket rules is BNAP; badges say which mixes are guaranteed to interoperate.
  • Reuse existing names. Schemes keep their native identifiers and wire formats; BNAP wraps them, never renames them.
  • Contain plugs, trust few. The socket alone computes digests, prefixes and commits use IDs and rail IDs, checks value against price and builds every message the signer signs. Only the store, the signer and the rails are trusted, and the operator supplies them.
  • The model never chooses. No language model picks a scheme, sets a cap or holds a key; spending policy lives in code.
  • End to end. Abuse is judged by the application; BNAP only makes prices and permissions verifiable.
  • No per-client state. The server stores records about credentials, never about people.
  • It’s OK to be odd. Unknown even extensions are rejected, unknown odd ones ignored, and GREASE keeps the rule alive.

Architecture: one socket, fourteen plugs

The socket is the only part every deployment shares; every other block is a plug the operator or the client chooses.

Figure: The universal plug · one socket, fourteen plug points. The source tab shows this as an interactive diagram (widget node/aeebbc11-bb14); it is not part of this export. See the source doc.

Plugs propose and the socket decides: schemes plug in from the registry, carriers carry the result, and client and server plugs supply everything else.

PlugSideWhat it doesOn the wireExamples
Credential schemeBothProves value, yields use IDs, settles change or refundsYesbnap:notes, L402, MPP Lightning, x402, ACT, Cashu NUT-24
Attestation schemeBothProves a property that waives, discounts or meters a priceYesPrivacy Pass, ARC, Web Bot Auth, OAuth with DPoP, NIP-98, World ID
Funding railServerCreates payment requests and reports their status. Operator-trustedPartlyPhoenixd, LDK Node, NWC, LND, CLN, Ark, Fedimint, Spark, Cashu mints, x402 facilitators, Stripe or Tempo
VerifierServerChecks a credential locally or through a facilitator, in two phases: verify, then settleNoLocal, x402 facilitator, MPP processor
CarrierBothDefines the scope and carries offers, credentials and receiptsYesHTTP, MCP, SASL, Nostr, gRPC, WebSocket, A2A
PricerServerMaps an action to a pricePrice onlyStatic table, load-based, per-token metering, fiat-reference feed
JudgeServerAdmits, queues or refuses before commit; hooks for abuse afterNoRules, WASM policy, classifier sidecar
Use storeServerAtomic records for single-use, rate-limited and reusable credentials and for rail payments. Operator-trustedNoPostgres, SQLite, FoundationDB, Redis with append-only log
SignerServerSigns only messages the socket built, and blind signatures up to the change cap the socket set; keys stay in its process. Operator-trustedKey IDs onlyFile, separate process, HSM, seed wrapped at rest by a KMS, threshold signer
FrontServerPuts the gate in front of an existing APINoTower layer, standalone proxy, Envoy ext_authz, Caddy forward_auth, nginx auth_request
Pay sourceClientPays a payment requestNoNWC, LDK, lnget, Cashu, Ark, Spark or Fedimint wallets, custodial APIs, cards via MPP
Spend policyClientCaps, allowed schemes, required propertiesNoBudget files, NWC budgets, AP2 mandates
WalletClientPicks an offer and builds the credentialNoNotes wallet, L402 client, x402 signer
Agent adapterClientGives any model the same three tools: paid fetch, budget, receiptsNoMCP server, OpenAI, Anthropic or Gemini tool definitions, LangChain, LlamaIndex, CLI

The socket

The socket is five objects and their rules: the Offer, the Credential envelope, the use record, the Receipt and the manifest. Every deployment MUST implement them exactly; everything else is a plug.

Normative text. Byte layouts, algorithms and test vectors now live in the Socket spec draft-00 tab, which wins where the two differ. Its section 15 lists the differences, among them: TLV instead of CBOR for the Credential, a body and witness split, a ReplayKey that covers bodies, route-scope offers, and rail IDs supplied by the rail rather than the scheme.

Offer

The Offer generalizes v0.4’s CHALLENGE. It is a TLV stream (Lightning BOLT 1 encoding, BIP-340 tagged hashes) that lists every scheme the server accepts for this action.

TypeFieldMeaning
0versionSocket version, 1
2scope32-byte hash of the action, defined by the carrier to cover content and recipient
8meteredOptional: the final charge is set at settlement, up to the chosen entry’s amount. Only entries whose scheme refunds atomically or has an up-to mode may appear
10expiresUnix time after which the offer lapses
12infoURI of the manifest
14op_key_idWhich online offer key, listed in the manifest, signed this offer
16nonce16 random bytes
18payAccepted value schemes in preference order. Each entry: scheme ID, amount, unit (sat, an ISO 4217 code or a CAIP-19 asset), quote expiry and scheme parameters
20attestAccepted attestations: scheme ID, parameters, and effect (required, waive, discount or quota)
24issued_atSelf-issued offers only
26nativeArtifacts the server made for one pay entry, such as an invoice, a macaroon or x402 requirements. Excluded from the OfferID; each embeds it
odd, reservedGREASEIgnored by every receiver; servers SHOULD include one
240offer_sigBIP-340 signature by the online offer key over every record below 240
242macServer MAC over every record below 240, so the server stays stateless

Types 240 to 255 are signature records, as in BOLT 12. The server builds an offer in two steps. It first computes OfferID = H_“bnap/offer”(every record below 240 except native), then makes each native artifact so that it embeds the OfferID: an invoice’s description hash, an L402 caveat, an MPP id, an x402 extra field. offer_sig and mac then cover everything below 240, natives included, so a client can check that the whole offer set is complete and authentic, and the server can check without state that it issued it. Each pay entry carries its own amount and unit: a route priced in sat and in USDC lists both, and the Pricer converts when it builds the offer. A credential’s value is compared only with its own entry. Clients MUST ignore unknown schemes.

Credential envelope

Credential  = {v, offer, pay?, attest[], binding, ext}           deterministic CBOR
pay         = {scheme, payload}                                   at most one
attest      = {scheme, payload}                                   zero or more

OfferID     = H_"bnap/offer"(offer records below 240, natives excluded)
Context     = H_"bnap/ctx"(OfferID ‖ value scheme ID ‖ sorted attestation scheme IDs ‖ H(ext))
Digest      = H_"bnap/cred"(Context ‖ binding ‖ H(pay payload) ‖ H(each attest payload))
ReplayKey   = H_"bnap/replay"(Context ‖ sorted socket use IDs)       32 bytes, stored with every use

A request carries at most one value proof and any number of attestations. A scheme that was not in the signed offer is rejected with NOT_OFFERED. The Context covers every chosen scheme and the extensions, so nobody who sees a credential in transit can strip an attestation or change ext without breaking it. Binding is empty or the RFC 9266 tls-exporter value. It enters the Digest but not the Context or the ReplayKey: only class S credentials are TLS-bound, and a retry on a new connection is still recognized as the same credential. Fronts that terminate TLS outside the gate, such as Envoy ext_authz and nginx auth_request, cannot pass the exporter value today, so routes behind them MUST NOT require TLS binding.

Every scheme ties its credential to the offer, and where it can to the request, in one of four registered binding classes. Who makes the artifact decides the class: an artifact the client makes after choosing can embed the Context, while one the server made before the client chose can only embed the OfferID.

ClassHow the credential is tiedExamples
SA signature over the Digest: offer, chosen schemes, extensions, payloads and TLS bindingbnap:notes, ACT
AA client-made artifact embeds the Context: offer, chosen schemes and extensionsx402 exact on EVM with nonce = Context, NIP-98 tags, a DPoP claim, a Web Bot Auth signed component, World ID signal, Hashcash
OA server-made artifact embeds the OfferID: one offer and its scope, but not the client’s other choicesL402 macaroon caveat, MPP challenge id, Lightning invoices (payment:lightning/charge, x402 on Lightning), Privacy Pass and ARC redemption contexts
NoneA bearer credentialAllowed only at price 0, and never with a waive or discount effect

Class O credentials are bearer within their offer: anyone who sees one before the server does can spend it on that action, so the registry records their theft property as bearer. The gate MUST NOT list in pay a scheme whose class is weaker than the route requires.

Uses and outcomes

Every credential yields use IDs. The socket prefixes each with the scheme ID itself, so no scheme can write into another’s namespace: bnap:notes:<Y>, httpauth:L402:<payment hash>. A scheme that relies on a rail payment MUST also report that payment’s canonical rail ID, such as ln:<payment hash> or evm:<chain>:<token>:<from>:<nonce>, and so MUST any scheme that mints credit from a payment. The socket commits rail IDs in the same atomic step as use IDs, so one Lightning payment offered under L402, MPP and x402 at once still admits one action. The scheme’s registered use model says how use IDs count:

Use modelRuleExamples
singleEach use ID admits one actionBlind notes, per-action Lightning, Privacy Pass
rate(k, epoch)At most k actions per use ID per epochARC, World ID nullifiers
reusable(quota)A metered allowance per use IDOAuth tokens, API keys, Web Bot Auth keys

The socket owns one use store and one outcome model for all schemes:

  • Verify. Each scheme checks its credential without side effects. A value scheme returns use IDs, rail IDs and a value in its pay entry’s unit; an attestation returns use IDs and its effect.
  • Price. The Pricer sets the price in the chosen entry’s unit. Attestations then apply in offer order: every waive first, then discounts, multiplied (two 20% discounts give 36% off, not 40%). Only attestations of class S, A or O may waive or discount. require applies to attestations as to value schemes.
  • Commit. The socket checks value against the effective price and records every value, attestation and rail use ID with the ReplayKey and status PENDING, in one transaction. Any conflict aborts the whole commit.
  • Act. The carrier runs the action and keeps the response for the replay window. A carrier that cannot keep it answers a replay with the stored receipt alone, never a further refund (draft-00, section 6.4).
  • Settle. Status moves exactly once to OK or FAILED. For metered actions, the charge is the amount used. On FAILED, value uses stay spent and the scheme refunds as its refund property allows; single-use attestations stay spent; rate counters are given back. A use still PENDING after the manifest’s pending_timeout (default 300 s) moves to FAILED.
  • Replay. A request whose use IDs are already committed is a replay only if its ReplayKey matches the record. The socket re-verifies the credential and checks its binding fresh, then returns the recorded outcome with replayed = true; the action never runs twice. Any other match is SPENT. A replay hands the response to whoever presents a valid credential, so the response is only as private as the scheme’s theft property.

Receipt

Every response carries the same receipt, whichever scheme paid:

Response = {status, replayed, scheme_result, ack}
ack      = BIP-340 signature by an online receipt key over
           H_"bnap/ack"(Digest ‖ status ‖ charged ‖ unit ‖ H(scheme_result))

An operator whose keys live in an HSM MAY add an ES256 signature as well; BIP-340 stays mandatory.

Errors

The socket’s codes are VERSION, MALFORMED, TOO_LARGE, BAD_OFFER, STALE, WRONG_SCOPE, NOT_OFFERED, POLICY (properties not met), SPENT, PENDING and RATE_LIMITED. Scheme errors are namespaced, such as bnap:notes/OUTPUT_REUSED.

Manifest and pinning

/.well-known/bnap is signed by an offline root key. It lists versions; the short-lived online offer and receipt keys, with their expiry; one backup root key; routes with prices and offers in preference order; carriers; limits such as replay_window and pending_timeout; a sequence number seq; and a max_age of at most 30 days.

Clients pin the root key and its backup, never per-route scheme lists. Scheme lists change whenever an operator pulls a broken adapter or a rail goes down, and pinning them would lock clients out, as HPKP did; offer_sig already stops anyone stripping a scheme from a BNAP offer. A client that meets an offer key it does not know refetches the manifest over HTTPS and accepts it if the root or the backup signed it with a higher seq.

A manifest MAY set bnap-required. A client that has seen it MUST NOT pay that origin through native headers until max_age lapses, as with HSTS. This is the only defence against the real downgrade, deleting the BNAP header so a client falls back to L402, MPP or x402, and a tier-0 client that never saw the manifest cannot detect it. First contact is trust on first use; operators who want more publish the root key out of band, in DNS, on Nostr or in their documentation.

Extensibility

TLV records follow the even/odd rule: an unknown even type is rejected, an unknown odd type ignored. GREASE TLV types are odd, so receivers keep ignoring them; scheme IDs and carrier names reserve GREASE values too. Types 240 to 255 hold signatures. Carriers that use JSON, such as MCP, canonicalize it with RFC 8785.

Scheme properties and route policy

Every registered scheme carries a property vector, and a route admits a scheme only if every property meets the route’s requirement. This is how flexibility stays safe: an operator can plug in any scheme, but a weak one can never quietly satisfy a strong requirement.

PropertyValues, weakest to strongest
usereusable → rate-limited → single, as the socket enforces it
bindingnone → O → A → S
theftbearer → key-witnessed
link-paymentlinkable to the payment → unlinkable from it
link-actionslinkable across actions → unlinkable across them
issuer, as the server trusts itthird party → facilitator → self-issued
verifyonline facilitator → offline
finalityreversible → freezable → final at settlement → final on verification
custody, as the client trusts itthird-party custodial → operator IOU → client self-custody
refundnone → out of band → atomic in protocol
pqno → yes
maturityexperimental → standard

Rules:

  • Property vectors come from registry entries, set from conformance results. Plugins MUST NOT declare properties at runtime.
  • Schemes, rails and verifiers each have entries. The gate computes each pay entry’s effective vector as the per-property minimum over its scheme, its rail and its verifier: notes funded by card are reversible and linkable to the payer, and x402 through a custodial facilitator is custodial.
  • An unknown or missing property counts as its weakest value.
  • The check is per property; there is no overall score. Link is two properties because an EVM address can be linkable to the payment and across actions at once.
  • Units are not a property: each pay entry carries its own, and a route’s price is a map of units.
  • The gate never lists in pay an entry whose effective vector fails the route’s require, and refuses to start if a route can be satisfied by none of its enabled schemes.
  • Experimental schemes need allow_experimental on each route that uses them.
[[route]]
match   = "POST /v1/chat"
price   = { sat = 21 }                       # a map of units; the Pricer converts
require = { use = "single", binding = "O", link-actions = "unlinkable" }
accept  = ["bnap:notes", "payment:lightning/charge", "httpauth:L402"]
attest  = [{ scheme = "httpsig:web-bot-auth", effect = "discount", percent = 20 }]

Here a reusable bearer token could never admit a chat request, however it was configured. Blind notes (class S) and per-action Lightning through MPP or L402 (class O) all qualify, and all three share one rail ID per payment. Raise the route to binding = "A" and the gate offers blind notes alone.

Negotiation

The server signs the list of what it accepts; the client picks one and never says what it supports. One 402 response carries the BNAP Offer plus each foreign scheme’s own native challenge, so clients that only speak L402, MPP or x402 still work unchanged.

HTTP/1.1 402 Payment Required
WWW-Authenticate: BNAP offer="<base64url Offer: scope, expires, nonce,
    pay=[bnap:notes 21 sat, payment:lightning/charge 21 sat, httpauth:L402 21 sat, GREASE],
    native=[invoice with description hash = OfferID, macaroon], offer_sig, mac>",
    info="/.well-known/bnap"
WWW-Authenticate: Payment id="<embeds OfferID>", realm="api.example", method="lightning",
    intent="charge", expires="…", request="…"
WWW-Authenticate: L402 macaroon="<caveat bnap_offer = OfferID>", invoice="lnbc210n1…"

POST /v1/chat …  Authorization: BNAP <base64url Credential{offer, pay: bnap:notes, binding}>
HTTP/1.1 200 OK  Authentication-Info: bnap="<base64url Response with ack>"

Client choice, in order:

  1. Verify offer_sig with an offer key the pinned root signed; on an unknown key, refetch the manifest.
  2. Keep the offers the client can use.
  3. Drop offers its spend policy forbids. By default a linkable scheme is used only when no unlinkable one is offered, or the user has opted in.
  4. Rank by total cost: each entry’s own amount converted to the budget’s unit, rail fees and a linkability penalty. The server’s order breaks ties.
  5. Send exactly one value credential and any attestations.

Native credentials. A tier-0 client pays through a scheme’s own header and sends no envelope. Every adapter MUST therefore embed the OfferID in its native artifact and check it on redemption, and the artifact must be one the server can authenticate without state (a macaroon, an HMAC’d MPP id, an invoice its own node issued); otherwise the server keeps the offer in a short cache keyed by nonce. The socket then builds a synthetic envelope: Context = H_“bnap/ctx”(OfferID ‖ scheme ID) and Digest = H_“bnap/cred”(Context ‖ H(native credential bytes)). Use IDs, rail IDs, replay and the BNAP receipt follow from it, and the receipt goes in Authentication-Info next to the scheme’s own. A request carrying more than one value credential, native or BNAP, is rejected.

Native challenges and abuse. Native challenges cost the server an invoice and a signature, so they must not turn an anti-spam protocol into an amplifier. A server MUST mint at most one invoice per offer nonce, MAY reuse one native challenge per scope for a short TTL, and SHOULD rate-limit minting per client address. It MAY instead send only the BNAP offer and mint natives when a client asks for a scheme, at the cost of tier-0 clients.

Plug catalogue

The abstraction was tested against every system in the table below, some rows covering several related systems. All but two fit as scheme, attestation, rail, carrier or client plugs: Cloudflare pay-per-crawl, whose payment side is closed, and AP2 as a direct per-action payment, though AP2 mandates work as a client spend policy. “Adapter” means the scheme fits once its artifact carries a BNAP value, such as an L402 caveat, an MPP id or an x402 nonce. Each is a convention the native spec already allows, but both sides must follow it.

SystemPlugs intoBindingUse modelFit
Blind notes (bnap:notes)CredentialSsingleNative, the default
L402 (bLIP-26)Credential + fundingO, OfferID caveatsingle, enforced by the socketAdapter
MPP (draft-httpauth-payment)Credential envelope + methodsO, OfferID in the HMAC’d idsingleAdapter
x402 on EVM chainsCredential + facilitator verifierA, client nonce = ContextsingleAdapter, guarded rail
x402 on LightningCredential + facilitator verifierO, invoicesinglePending a published spec
Cashu NUT-24, NUT-18, NUT-26Credential + carrierO when locked to the operator’s key, else NonesingleAdapter; unlocked only at price 0
Cashu NUT-22 blind authCredentialNone, bearersingleNative at price 0
ACTCredentialSsingleNative, reserved
Privacy Pass (RFC 9576–9578)AttestationO, redemption contextsingleNative
ARCAttestationO, presentation contextrateNative
Web Bot AuthAttestationA, Context as a signed componentreusableAdapter
OAuth and API keys, with DPoPAttestationA with a DPoP claim, else NonereusableAdapter; without DPoP, never waive or discount
NIP-98Attestation + Nostr carrierA, tagsreusableAdapter
World IDAttestationA, signalrateAdapter
Hashcash, AnubisCredential, unit workAsingleAdapter, weak
NWC (NIP-47)Pay source, funding rail——Native
Fedimint, Ark, SparkPay source, funding rail——Adapter
MCP, A2ACarrier, agent adapterScope from tool and arguments—Adapter
AP2 mandatesClient spend policy——Adapter
Cloudflare pay-per-crawlPricer signal only——Does not fit

Naming and the registry

Scheme IDs reuse existing registries as <registry>:<id>[@<qualifier>], so BNAP never renames anything:

PrefixSource registryExamples
bnap:BNAP’s own schemesbnap:notes
payment:HTTP Payment Methods and Intents (draft-httpauth-payment)payment:lightning/charge
x402:x402 schemes and networks as x402 names them: CAIP-2 for EVM chains; lnbtc is x402’s own label, not CAIP-2x402:exact@lnbtc, x402:exact@eip155:8453
pp:Privacy Pass token typespp:0x0002
httpauth:HTTP authentication schemeshttpauth:L402, httpauth:DPoP
httpsig:HTTP message signature tagshttpsig:web-bot-auth
cashu:, nip:Cashu NUTs, Nostr NIPscashu:nut24, nip:98
vnd.Unregistered vendor schemesvnd.example.pow

The registry starts as a file in the BNAP repository: each entry needs a public specification, a property vector set by conformance results, and the maintainer’s review as designated expert. It moves to IANA once two independent implementations exist.

The default scheme: bnap:notes

bnap:notes is the v0.4 Cashu profile, now one plug among many and the recommended default, because it is the only scheme that is unlinkable, verified offline and refunded atomically, all at once. Its full specification is the second tab; the changes for v0.5 are small.

How it works, in one paragraph. The service runs its own Cashu-style issuer. A client buys credit over Lightning (Cashu NUT-23, NUT-04, NUT-20) and holds it as Notes: Cashu proofs whose secret is the hex of a public key K. To spend, it signs the socket’s Digest with each K and sends blinded outputs for change and refunds. The issuer checks the notes offline, and the socket records each note’s Y as a single-use ID. Wallets recover from a seed through NUT-13, NUT-09 and NUT-07.

PropertyValue
use / binding / theftsingle / S / key-witnessed
link-payment / link-actionsunlinkable, except that the first action after funding can be correlated by timing / unlinkable
issuer / verifyself-issued / offline
finality / custodyfinal on verification / operator IOU
refundatomic in protocol
pq / maturityno / experimental until the NUTs are accepted

These are the scheme’s own entries. Funded by a card or another reversible rail, the effective vector drops to reversible and linkable to the payer, as the rule on effective vectors requires. The notes unit is sat.

What moved into the socket: the CHALLENGE became the Offer, with the notes-specific fund_quote now a parameter of the notes offer; witnesses sign the socket Digest; commit, replay, receipts and the core error codes are the socket’s; scheme errors are namespaced bnap:notes/….

What stays in the scheme: the Note format, key isolation, input fees, refund outputs, funding inside the 402, seed recovery, the spend-store layout and the BNAP-W withdrawal extension.

Value rails

Rails are plugs: BNAP fixes how a payment request is asked for and confirmed, never which network or provider carries it. Lightning BOLT11 is the default rail and part of the Core-1 badge; everything else is the operator’s choice.

RailFinalityStatus in the reference implementation
Lightning (Phoenixd, LDK Node, NWC, LND, CLN, LSPs)FinalDefault
Ark, Spark, Fedimint, Cashu mintsFinal once settled; custody variesOptional plugs
On-chain BitcoinFinal after confirmationsOptional plug
Stablecoins (x402 on EVM chains, Tempo)Final at settlement, freezable by the issuerGuarded, off by default
Cards (MPP through Stripe)Reversible for monthsGuarded, off by default

Guardrails for non-Bitcoin rails. These rails plug in freely, under rules that keep them from weakening anyone else:

  • They are off by default: a build feature plus an explicit setting turns each one on.
  • Credit they fund carries its own unit (usd, eur or a CAIP-19 asset) and is never mixed into sat credit.
  • A reversible rail may only fund credit, never admit an action directly, and that credit is held until the payment is final or capped per epoch. A stolen card must not be able to buy spam.
  • The manifest marks every such rail with its finality and custody properties, so clients and route policies see them.
  • Facilitators (x402) are verifier plugs with their trust level declared: non-custodial relay, custodial, or merchant of record.

Carriers

A carrier defines two things: the scope function that names one action, and where offers, credentials and receipts travel. Any protocol with a slot for them can carry BNAP.

CarrierScope coversOffer, credential and receipt travel inStatus
HTTPMethod, normalized absolute URI, Content-DigestWWW-Authenticate, Authorization, Authentication-InfoCore-1
MCPServer URI, tool name, RFC 8785 canonical JSON of the argumentsJSON-RPC error data, request _meta, result _metaAgent-1 badge
SASLService, mechanism and server nonceSASL challenge and responseOptional
NostrRelay URL and event IDEvent tag and relay OK message; offers self-issuedOptional
gRPCService, method and message digestMetadataOptional
WebSocketEndpoint and message digestPer-message envelopeOptional
A2AAgent card and task digestTask metadataOptional

Recent MCP revisions favour stateless servers, which suits BNAP: an Offer carries everything needed to verify the credential later. TLS binding is not available through most MCP hosts, so MCP credentials rely on scope binding.

Agents and clients

BNAP never names a model, a framework or a wallet. Every agent adapter exposes the same three tools to any model that can call tools: fetch_paid, budget and receipts.

Client plugChoices
Agent adapterMCP server, OpenAI, Anthropic or Gemini tool definitions, LangChain, LlamaIndex, the bnap CLI
WalletNotes wallet with seed recovery, L402 client, x402 signer, any third-party wallet that implements a scheme
Pay sourceNWC, LDK, lnget, Cashu, Ark, Spark or Fedimint wallets, custodial APIs, cards through MPP
Spend policyBudget files, NWC budgets, AP2 mandates, organisation rules

Rules for every client, whatever its plugs:

  • Spending caps per origin and per task are enforced in code. A model never picks a scheme, sets a cap or holds a key, and a prompt-injected “pay with X” goes through the same policy check as anything else.
  • The client verifies offer_sig with a key its pinned root signed before paying, and honours bnap-required.
  • It never advertises which schemes it supports.
  • It keeps the receipt of every paid action.
TierThe client hasExample
0Any single-scheme clientlnget for L402, an x402 or MPP client
1A BNAP wallet plus any pay sourcebnap get with an NWC connection
2A BNAP wallet with its own seed and several schemesPicks per route by properties and cost

Reference implementation: Caparra as a plug host

Caparra is a host for plugs: the socket in one small crate, every plug point a Rust trait, and three ways to load a plug so that no operator is tied to Rust or to the project’s own choices.

CrateRole
bnap-coreThe socket: Offer, envelope, Digest, use store logic, outcome state machine, receipts, test vectors
bnap-plugThe plug traits, plus the WIT interface definition generated from them
bnap-scheme-*notes, l402, mpp, x402, privpass, webbotauth, oauth
bnap-rail-*nwc, phoenixd, ldk, lnd, ark, fedimint, plus off-by-default x402-evm and mpp-card
bnap-store-*, bnap-signer-*, bnap-carrier-*Postgres and SQLite; file, process and KMS-unwrap signers; HTTP, MCP, SASL, Nostr
caparra-gate, caparra-wallet, bnap-mcp, bnap-cliThe gateway, the client wallet, the MCP server and the CLI
#![allow(unused)]
fn main() {
#[async_trait] pub trait CredentialScheme: Send + Sync {
    fn id(&self) -> SchemeId;                         // properties come from the registry, not from here
    async fn offer(&self, cx: &OfferCtx) -> Result<SchemeOffer>;        // any native artifact embeds cx.offer_id
    async fn verify(&self, c: &Credential, cx: &VerifyCtx) -> Result<Verified>;  // no side effects; cx.now, cx.signer
    async fn settle(&self, v: &Verified, out: Outcome, s: &CappedSigner) -> Result<SchemeResult>;
}
pub struct Verified { pub use_ids: Vec<LocalUseId>,  // bnap-core prefixes these with the scheme ID
                      pub rail_ids: Vec<RailId>,     // ln:<payment hash>, evm:<chain>:…; committed with the use IDs
                      pub value: Amount }            // in the chosen pay entry's unit

pub trait Carrier { fn scope(&self, r: &Request) -> Scope; fn extract(&self, r: &Request) -> Option<RawCredential>;
                    fn offer(&self, o: &SignedOffer) -> Response; fn receipt(&self, r: &Receipt, out: &mut Response); }
#[async_trait] pub trait FundingRail { async fn request(&self, a: Amount, memo: [u8; 32]) -> Result<PayRequest>;
                                      async fn status(&self, id: &PayId) -> Result<PayState>; }   // operator-trusted
#[async_trait] pub trait UseStore {                                                              // operator-trusted
    async fn commit(&self, ids: &[UseId], key: ReplayKey) -> Result<Commit>;     // New | Replay | Conflict, all or nothing
    async fn finalize(&self, ids: &[UseId], key: ReplayKey, s: Final) -> Result<()>;
    async fn expire_pending(&self, older_than: Duration) -> Result<u64>; }      // PENDING past pending_timeout -> FAILED
#[async_trait] pub trait Signer {                                                                // operator-trusted
    async fn schnorr(&self, k: KeyRef, msg: [u8; 32]) -> Result<Sig>;            // msg built by bnap-core only
    async fn verify_unblinded(&self, ks: KeysetId, y: Point, c: Point) -> Result<bool>;
    async fn blind_sign(&self, ks: KeysetId, outs: &[(Amount, Point)], cap: Amount) -> Result<Vec<(Point, Dleq)>>; }
    // refuses when outs sum above cap; bnap-core sets cap = value - charged
#[async_trait] pub trait Pricer { async fn price(&self, a: &Action, unit: &Unit) -> Result<Amount>; }
#[async_trait] pub trait Judge  { async fn admit(&self, a: &Action, p: &Properties) -> Decision; }
#[async_trait] pub trait PaySource { async fn pay(&self, r: &PayRequest, b: &Budget) -> Result<PayProof>; }
}

Only bnap-core computes the OfferID, Context, Digest and ReplayKey, prefixes use IDs, compares value with price, commits uses and builds every message the signer signs. A scheme gets a CappedSigner that can verify notes and sign blinded outputs up to the change cap bnap-core computed, and nothing more. Every other plug gets read access to what it needs and returns data.

Three ways to load a plug:

WayForIsolationRelease
Compiled in (Cargo features)First-party plugsSame processv0.5
Sidecar over gRPC or HTTP, same interfaceAny language, heavy dependencies such as an x402 facilitator, LND or a Python classifierSeparate processv0.6
WASM component (WIT world bnap:plug, WASI 0.3)Third-party schemes, pricers and judgesSandbox with fuel and memory limits, no ambient networkv0.7

Fronts and bindings. Operators on any stack put caparra-gate in front as a standalone proxy, Envoy ext_authz, Caddy forward_auth or nginx auth_request, so they need no Rust at all. Client libraries ship through UniFFI (Swift, Kotlin, Python), napi-rs (Node) and wasm-bindgen (browsers).

One config, any mix:

[gate]     origin = "https://api.example"; front = "envoy-ext-authz"; replay_window_s = 86400; pending_timeout_s = 300
[manifest] root_key = "file:/etc/caparra/root.pub"; backup_key = "file:/etc/caparra/backup.pub"; max_age_s = 604800; bnap_required = true
[store]    kind = "postgres"; url = "env:DATABASE_URL"; durability = "sync"
[signer]   kind = "process"; socket = "unix:///run/caparra-signer.sock"; seed_wrapped_by = "aws-kms"
[[rail]]   id = "ln";  kind = "phoenixd"; url = "http://127.0.0.1:9740"
[[rail]]   id = "ln2"; kind = "nwc"; uri = "env:NWC_RECEIVE_ONLY"
[[scheme]] id = "bnap:notes";               rails = ["ln", "ln2"]; max_fund_sat = 10000
[[scheme]] id = "httpauth:L402";            rail = "ln"
[[scheme]] id = "x402:exact@eip155:8453";   unit = "eip155:8453/erc20:0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913"
                                            facilitator = "https://fac.example"; enabled = false
[[attest]] id = "httpsig:web-bot-auth"
[pricing]  kind = "table"; file = "prices.toml"
[judge]    kind = "sidecar"; url = "grpc://classifier:7001"

Plugin security

Plugs may come from anyone, so the host assumes any plug can be buggy or hostile and limits what it can reach.

  • Two trust levels. The use store, the signer and the funding rails are operator-trusted: the operator supplies them and the gate runs only versions it pinned. Schemes, attestations, verifiers, carriers, pricers and judges are contained.
  • What a hostile scheme can do. It can admit unpaid actions on the routes that enable it, as any broken payment check could. It cannot write another scheme’s use IDs, because bnap-core prefixes them. It cannot reuse a payment another scheme used, because bnap-core commits rail IDs. It cannot get a signature on a message bnap-core did not build, or blind signatures above the change cap. Enable third-party schemes only on routes whose losses you accept.
  • No keys. No plug sees an issuer key or a note key. The signer itself verifies unblinded notes and signs blinded outputs.
  • Sandboxes. Third-party plugs run as WASM components with only the capabilities their WIT world declares, CPU and memory limits, and no ambient network or files. The host passes the current time in each call, so expiry checks still work. No plug runs inside the signer process.
  • Supply chain. Plugs are signed (Sigstore), logged in a transparency log and built reproducibly. The gate pins plug hashes and checks a revocation list, and refuses a known-broken version even when configured.
  • Safe defaults. Experimental schemes and non-Bitcoin rails stay off until turned on per route.

Badges, profiles and conformance

Any deployment that keeps the socket rules is BNAP, whatever its plugs. Badges label the mixes that are guaranteed to work together, so flexibility never costs interoperability for those who want it.

BadgeRequires, on server and clientGuarantees
Core-1The socket, bnap:notes, HTTP, Lightning BOLT11 fundingAny two Core-1 parties can transact with notes that are unlinkable across actions. Provisional until the notes NUTs are accepted; until then Core-1 is the one place an experimental scheme is on by default
Agent-1Core-1 plus the MCP carrier and httpauth:L402 or payment:lightning/chargeAgents with no BNAP code can pay over Lightning, and MCP tools can be priced. payment:lightning joins as of right once it is registered; x402 on Lightning once its spec is published
Fiat-FundingA separate unit, credit held until finalCards and stablecoins can fund credit without weakening sat credit
Nostr-1, SASL-1The carrier profilePriced relay writes; priced mail and chat

An L402-only or x402-only deployment that keeps the socket rules is still BNAP, just without a badge.

Conformance is the product. A badge is earned by passing the public test suite, not by a claim. The suite includes negative tests: stripped offers, cross-scheme replay, a wrong scope, a status flip on replay, an unsatisfiable route, and a plug trying to sign or commit.

Security and privacy considerations

Flexibility adds threats a single scheme does not have; each has a socket rule.

ThreatClosed by
A proxy strips the strong scheme from an offerSigned offers; the chosen schemes inside the Context
A proxy deletes the BNAP header so the client pays nativelybnap-required for clients that saw the manifest. A tier-0 client cannot detect it
A weak scheme satisfies a strong routeRegistry vectors, effective vectors over scheme, rail and verifier, per-property checks, unknown = weakest
One payment admits actions through several schemesCanonical rail IDs committed with the use IDs; one value credential per request
An observer re-sends a credential with altered ext or attestationsThe Context covers attestation IDs and ext; bearer attestations cannot waive or discount; class O is registered as bearer
A reusable token treated as single-useUse model from the registry, enforced by the use store
Stolen cards fund spamReversible rails fund credit only, in their own unit, held until final
A hostile scheme admits unpaid actionsContained to the routes that enable it; prefixed use IDs, rail IDs and the change cap keep it from anything else
A hostile plug forges receipts or inflates notesNo plug reaches a key; the signer signs only bnap-core’s messages and change within the cap
Native challenges used as a DoS amplifierOne invoice per offer nonce, cached or lazy natives, rate limits
A first-contact attacker pins a hostile keyPinning only for HTTPS-fetched manifests, max_age capped at 30 days, backup key, higher seq replaces
A crash leaves uses PENDINGpending_timeout moves them to FAILED
A route that nothing can satisfyThe gate refuses to start
Prompt injection steers an agent’s paymentSpend policy in code; the model never chooses
Clients fingerprinted by their schemesClients never advertise capabilities
Privacy split across many schemesAt most one privacy-bearing scheme per unit per origin is RECOMMENDED

The scheme-level threats and residual risks of blind notes stay as specified in the second tab. Two residual risks are new: each linkable scheme an operator enables lowers the privacy of the clients who use it, and each facilitator or custodial rail adds a party who can censor or freeze payments. Route policies and client policies make both visible; they cannot remove them.

Roadmap and standardization

The socket ships first, with the plugs agents need on day one; the plug runtimes and the registry grow with adoption.

ReleaseAdds
v0.5The socket in bnap-core; compiled-in plugs: bnap:notes, payment:lightning/charge, httpauth:L402; OAuth with DPoP and Web Bot Auth as attestations; HTTP and MCP carriers; NWC and Phoenixd rails; the conformance suite
v0.6Sidecar plugs over gRPC or HTTP; the x402 adapter, on Lightning once its spec is published; SASL and Nostr carriers; Ark and Fedimint rails; guarded fiat funding behind the Fiat-Funding badge
v0.7WASM component plugs; Privacy Pass, ARC and ACT schemes

Documents, in order:

  1. The BNAP socket (about 15 pages): Offer, envelope, use and outcome model, receipt, manifest, properties, registry rules. Published in the repository, later an IETF individual draft.
  2. NUT-XX key-secret proofs and NUT-XX HTTP 402 with a self-issued mint: the bnap:notes scheme, for the Cashu NUTs.
  3. HTTP Payment Method registration of cashu, and support for registering lightning, in the empty registries of draft-httpauth-payment.
  4. The MCP carrier note, aligned with the MPP and x402 MCP transports.
  5. NIP-XX priced relay writes, then SASL.

The registry lives in the repository with the maintainer as designated expert until two independent implementations exist; then it moves to IANA.

Appendix A: Debate record

Round 4, 11 October: the universal plug

A plug architect designed the socket and plug points, a flexibility skeptic attacked them, and an ecosystem mapper tested them against the systems in the plug catalogue. Where they still differed, the owner decided.

QuestionPositionsVerdict
Where can plugs attach?Architect: anywhere. Skeptic: anywhere except the credential, which only registered suites may fillAnywhere, but credential and attestation schemes need registry entries with property vectors
Who states a scheme’s properties?The plugin, or a registryThe registry, set by conformance results; unknown counts as weakest
Offer authenticationUnsigned (v0.4), or a signed offer set plus pinningSigned offer set plus pinning; a blocker even before v0.5
BaselineNotes mandatory everywhere, or no baselineOwner: a badge, not a gate (Core-1)
Non-Bitcoin railsInto sat credit, a separate unit held until final, or noneOwner: pluggable with guardrails
Credentials per requestOne scheme, or one payment plus attestationsOne value credential plus any attestations
Reusable credentialsExclude them, or model themUse models: single, rate-limited, reusable
Scheme namesA new namespace, or existing registriesExisting registries with prefixes
Plug runtimeNative only, or WASM for third partiesCompiled now, sidecars next, WASM after
NameRename, or keepOwner: keep Blind-Note Admission Protocol

Earlier rounds

RoundSettled
1, 9 OctoberThe operator issues its own notes; per-action prices over refundable bonds; application carriers first
2, 10 OctoberOne primitive, three verbs; the signature covers the whole spend; bonds and transport binding leave the core
3, 10 OctoberNotes become a Cashu profile with seed recovery, NUT-23 funding, refund outputs and recomputed replies

Independent reviews of v0.1, v0.3, v0.4 and v0.5 found fifteen blockers between them, all fixed.

Review of v0.5, 11 October

FindingFix in v0.5.1
Blocker: one Lightning payment offered under L402, MPP and x402 could admit one action per schemeSchemes report canonical rail IDs; the socket commits them with the use IDs
Blocker: the Context was circular for artifacts the server makes before the client choosesOfferID; class O replaces N; class A only for client-made artifacts
Blocker: payments through native headers had no Context, Digest or replayAdapters embed the OfferID; the socket builds a synthetic envelope
Blocker: “a hostile plugin can never cheat” was falseTrusted store, signer and rails; prefixed use IDs; a capped signer for change; the claim restated
Digest malleable by anyone who saw the credentialContext covers attestation IDs and ext; no waive or discount for bearer attestations
Attestation effects undefinedWaive, then multiplied discounts; one transaction for every use ID; rules on FAILED
Replay broken across connections and crashesReplayKey without binding; re-verification; cached responses; pending_timeout
One unit per offerAmount and unit per pay entry; price as a map of units
Metered actions overpaidOnly refundable or up-to schemes; the ack covers the charge
Pinning scheme lists repeats HPKPPin the root key only; bnap-required; tier-0 limit stated
Properties depend on rail and facilitatorEffective vectors computed by the gate
Invoices as a DoS amplifierOne invoice per nonce, cached or lazy natives, rate limits
MinorTwo link properties, viewpoints labelled; None rule aligned with Cashu; lnbtc labelled; badges provisional; catalogue claim corrected; odd GREASE, signature range, RFC 8785; fronts without TLS binding; KMS wraps the seed at rest; time passed to WASM

Open questions

  • Socket byte layout. Define the Offer’s pay and attest record encodings and publish test vectors.
  • Foreign headers. Verify the current x402 v2 header names and the L402 and MPP challenge syntax before shipping their adapters.
  • Registry seed. Write the first property vectors for every scheme in the catalogue and the conformance tests behind them.
  • First gateway routes. The first gateway is an LLM inference proxy: set metered prices per model, the maximum tokens per offer, and which schemes each route accepts.
  • Cashu maintainers: NUT numbers, the NUT-13 derivation type, alignment with NUT-24.
  • HTTP Payment Methods: draft the cashu registration.
  • KMS signing. AWS KMS cannot produce BIP-340 signatures, so the seed is KMS-wrapped at rest and lives in the signer process; confirm that is acceptable, or use a threshold signer. x402 on Lightning: confirm its network label and whether a client can choose a nonce, which would move it from class O to A.
  • Legal reading of non-withdrawable credit, BNAP-W, and fiat-funded credit.

Sources

Opened during round 4:

Sources for earlier rounds are listed in the other tabs.