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: Blind-Note Admission Protocol — Draft Specification v0.3

2026-10-10 · Dominic

This tab keeps version 0.3 of 10 October 2026 unchanged. The first tab, version 0.4, supersedes it.

Abstract and status

BNAP (Blind-Note Admission Protocol) is a minimal open protocol that lets any service attach a small Bitcoin-denominated price to an action and verify payment offline, without learning who paid. Its single primitive is the Note: a 74-byte, blind-signed, single-use public key that the service issued itself. Funding, deposits, pricing policy and carriers are separate layers or extensions, the way TCP/IP keeps IP small and builds everything else on top.

Name and notation. BNAP, said “bee-nap”, is the protocol; Caparra is its Rust reference implementation, as Apache implements HTTP. The name says what it does: a blind-signed note buys admission to an action. Notation follows TCP/IP: a slash means “over”, with the lower layer on the right, and a plus means “carried in”.

WrittenRead as
BNAP/LNBNAP notes funded over Lightning
BNAP/Ark, BNAP/CashuBNAP notes funded over Ark or Cashu ecash
HTTP+BNAPHTTP carrying BNAP in its authentication slot
HTTP+BNAP/LNThe full suite: HTTP with BNAP, funded over Lightning
SASL+BNAPSMTP, IMAP or XMPP carrying BNAP through SASL
BNAP-BThe deposit extension (refundable bonds)

Never write HTTP/BNAP: it claims HTTP runs over BNAP and reads like a version number. BNAP was chosen on 10 October over CPR (Cost Per Request), because it was the only finalist that collided with nothing in the IETF, Bitcoin or package registries.

Status. Pre-draft, version 0.3. A second debate round asked one question: what is the smallest core that could become foundational, like IP or TLS? Version 0.2, with its full detail on bonds, forfeit logs, pricing and the TLS binding, stays unchanged in the “v0.2 archive” tab as source material for the extension documents. The key words MUST, SHOULD, RECOMMENDED and MAY are to be read as in BCP 14 (RFC 2119, RFC 8174).

What round 2 changed.

  • One primitive replaces six proof types, three instruments and a session state machine.
  • Three verbs: CHALLENGE, SPEND and ISSUE. A session becomes an application cookie set after a SPEND.
  • The server keeps no per-client state: only records about notes, deleted when their keyset expires.
  • Refundable bonds, the forfeit log, pricing formulas, the TLS/QUIC binding and five of six funding rails leave the core for extensions or operator policy.
  • The core gained three things the debate found it cannot do without: the spend signature covers the whole message, every SPEND and ISSUE response carries a signed acknowledgement that serves as the receipt, and a retried SPEND returns the stored answer without repeating the action.

Owner decisions carried forward: Bitcoin-denominated and rail-agnostic; unlinkable by default; agents welcome if they pay; pluggable funding (now value-in types); refundable bonds with an operator-chosen forfeit destination and public claim rates (now the BNAP-B extension); Rust; MIT/Apache-2.0; Cashu NUT first, IETF later.

Why BNAP exists

BNAP removes actions that are only profitable at a price of zero: slop posts, spam mail, bulk fake signups, Sybil swarms. An automated action stops paying the moment its price exceeds its value to the actor:

p > v_{action}

The price is charged per action, not per actor, so splitting into many identities saves nothing. That is the defensible core of the original thesis. BNAP does not tell humans from machines; it makes each action cost something, and only low-value actions disappear.

ActorOutcome under BNAP
Slop and spam farmsPriced out once the price exceeds revenue per action
Sybil swarmsNo gain from splitting; the cost is per action
Paying agentsAdmitted at the same price as anyone
Capital-rich or state actorsNot deterred; out of scope
Volumetric floodsOut of scope
The issuing serviceCannot link a spend to the purchase that funded it

Money beats proof-of-work because it costs everyone the same, while hashcash favours GPUs over phones (Laurie and Clayton, 2004). Paying sats is a transfer, not proof of energy, so this spec argues from spam economics only. The BNAP-B extension adds a forfeitable deposit, which raises the left side by the deposit times the chance of being flagged, wherever an operator can detect abuse.

Design principles

Seven principles decide every trade-off; each is borrowed from a protocol that became foundational.

  1. One narrow waist. Many value rails below, many carriers above, one object in the middle: the Note. As with IP, the less the waist assumes, the more it can run on (Beck’s hourglass theorem).
  2. End to end. Judging abuse needs application knowledge, so it stays in the application (Saltzer, Reed and Clark, 1984). BNAP only makes a price verifiable.
  3. No per-client state. The server stores records about notes, never about people, as Bitcoin stores only the UTXO set. Challenges are MAC’d cookies, like TCP SYN cookies and QUIC Retry tokens.
  4. Pay rarely, admit offline. Money moves on a value rail when notes are bought; admission is a local check on every action. TLS draws the same line between its costly handshake and its cheap record layer.
  5. The relying party is its own issuer. A service accepts only notes it signed, so nobody else can mint admission and no allowlist or gatekeeper exists.
  6. Sign everything once. Each note key signs a hash of the entire spend, as TLS Finished covers the whole transcript.
  7. It’s OK to be odd. Extensions are TLV records: an unknown even type is critical and rejected, an unknown odd type is ignored (Lightning BOLT 1). Clients send random odd GREASE types so the mechanism never rusts (RFC 9170).

Non-goals: proving personhood; stopping well-funded attackers or volumetric DDoS; replacing the operator’s abuse classifier; being a payment network or a token. BNAP has no coin of its own.

Where BNAP sits in the stack

BNAP has one responsibility: admit one action at one origin if and only if the request carries unspent notes, issued by that origin, worth the posted price and signed over the whole request, checked offline and without learning who paid.

Figure: The BNAP hourglass · five layers, one waist. The source tab shows this as an interactive diagram (widget node/abfa0928-4b42); it is not part of this export. See the source doc.

Money crosses the waist only through ISSUE, rarely; every priced action crosses it through SPEND, as a local check with no payment.

BNAP cannot be HTTPS-shaped. TLS is transparent to HTTP; BNAP cannot be, because it must know which request is an action and what that action costs. It therefore lives in the authentication slot of existing protocols, as SASL mechanisms and HTTP authentication schemes do. It needs no new URI scheme, which would split web origins (RFC 6454), and no ALPN identifier. The layer names avoid Bitcoin’s L1/L2 vocabulary:

LayerContentsResponsible for
PolicyPrices, abuse judgement, deposits, sessionsEverything that needs application meaning
CarrierHTTP authentication scheme, SASL, Nostr, ESMTPPutting the bytes into a protocol slot
AdmissionBNAP: the Note and three verbsProving issuer-scoped value was spent once, for this request
ValueLightning, Ark, Cashu ecash, plus work and attested grantsMoving value, escrow, refunds
SettlementBitcoinFinal settlement

What BNAP does not do, as IP does not guarantee delivery:

  • A valid spend is necessary, not sufficient; the operator may still queue or refuse.
  • It moves no money and holds no custody; value rails do that.
  • It sets no prices; it carries the operator’s quote.
  • It identifies nobody and judges no abuse.
  • It provides no confidentiality; TLS does.
  • It does not stop volumetric floods or attackers who earn more per action than they pay.

The Note

A Note is a blind-signed, single-use public key worth a power of two units. It is the only object every BNAP implementation must agree on. It has no version, kind, session or action field: a field that does not exist cannot ossify.

 0                   1                   2                   3
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                     Keyset ID (64 bits)                       |
|      byte 0 = suite; the keyset fixes unit, keys, expiry      |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|   Exponent    |                                               |
+-+-+-+-+-+-+-+-+    Issuer signature C (33 bytes, compressed)  ~
~                                                               ~
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|        Note key K (32 bytes, x-only) = the nullifier          ~
~                                                               ~
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

Suite 0 Note = 8 + 1 + 33 + 32 = 74 bytes.  Value = 2^Exponent units.

Suite 0 (mandatory to implement): secp256k1, the blind Diffie-Hellman key exchange (BDHKE) used by Cashu (NUT-00), BIP-340 Schnorr signatures and SHA-256. The suite byte sets every field length after it, so a post-quantum suite can use larger keys and signatures without a new Note format.

Making and spending a Note (Suite 0)

  1. The client generates a fresh key pair (k_n, K) and computes Y = hash_to_curve(K).
  2. It blinds Y as B’ = Y + r·G and sends B’ in an ISSUE or as a change output.
  3. The issuer returns C’ = k_e·B’ with a DLEQ proof that it used the published key for exponent e. The client MUST verify the DLEQ, then unblinds: C = C’ − r·(k_e·G).
  4. To spend, the client presents (Keyset ID, e, C, K) and signs the spend digest with k_n.
  5. The issuer checks C = k_e·hash_to_curve(K) with its private key, checks the BIP-340 signature by K, and test-and-sets K in its spent-set.

The math is Cashu’s; the only change is that the secret is the Note key K itself, which removes the need for a separate P2PK lock (NUT-10, NUT-11).

What this guarantees:

  • Unlinkable. The issuer never sees K before the spend and never sees r, so it cannot link a spend to the ISSUE that funded it.
  • No DLEQ leak. The DLEQ proof is checked at issuance and never presented, so it cannot reveal the blinding factor.
  • Theft-proof. K must sign, so a copied Note is useless without k_n.
  • One nullifier rule. K is the nullifier for every Note.

Rules:

  • Keyset ID = suite byte ‖ first 7 bytes of SHA-256(suite ‖ unit ‖ expiry ‖ keys in exponent order). Clients MUST compare full key lists, not IDs, since a short ID can be ground to collide.
  • Consistent keys. Keysets are few, long-lived and identical for every client. Operators SHOULD publish a keyset at least 30 days before issuing from it, and clients SHOULD compare keysets from a second vantage point, so per-user tagging keys are caught.
  • Canonical encodings. K is a valid x-only key; C is a canonical compressed point and never infinity. Because K is x-only, K and −K are one nullifier.
  • hash_to_curve is Cashu’s function (NUT-00) applied to the 32 bytes of K, so BNAP shares test vectors and libraries with Cashu.
  • Powers of two. A spend of 13 units uses Notes of 8, 4 and 1.
  • One origin, once. A key K is used at exactly one origin and never twice.

The protocol: three verbs

BNAP has three verbs. ISSUE runs rarely and moves value; CHALLENGE and SPEND run on every priced action and need no network call beyond the request itself.

Figure: HTTP+BNAP/LN message flow · funding once, then per action. The source tab shows this as an interactive diagram (widget node/f507def9-5228); it is not part of this export. See the source doc.

Solid arrows are requests and dashed arrows are responses. Only the top band touches Lightning; the bottom band repeats for every priced action and never leaves the operator.

Encoding

BNAP messages are binary and identical on every carrier, which moves them as base64url.

  • Integers are big-endian; lengths and counts use BigSize, as Lightning BOLT 1 does.
  • A TLV stream is a run of (BigSize type, BigSize length, value) records in strictly ascending type order, with no duplicates.
  • Hashes are BIP-340 tagged hashes: H_tag(x) = SHA-256(SHA-256(tag) ‖ SHA-256(tag) ‖ x).

CHALLENGE (server to client)

A challenge is a TLV stream. The server MACs it so it keeps no state between CHALLENGE and SPEND.

TypeFieldMeaning
0versionProtocol version, 1
2scope32-byte hash of the action; each carrier defines it to cover the action’s content and its recipient
4amountThe price
6unitUnit string, such as sat
8keysetsAccepted keyset IDs, 8 bytes each
10expiresUnix time after which the quote lapses
12issueURI of the ISSUE endpoint
14op_key_idWhich operator key signs acknowledgements
16nonce16 random bytes
18macServer MAC over every record above
20issued_atSelf-issued challenges only: creation time

Self-issued challenges. Store-and-forward carriers such as Nostr events and relayed mail cannot do a round trip. Where the manifest allows it, a client MAY build a challenge with version, scope, amount, unit, keysets, its own nonce and issued_at, and no mac. The operator accepts it if amount is at least the price in its published price table and the age is within that carrier’s max_age_s. Replay is impossible, because each K is recorded with its Digest.

SPEND (client to server)

Note      = keyset_id(8) ‖ exponent(1) ‖ C(33) ‖ K(32)
Output    = keyset_id(8) ‖ exponent(1) ‖ B'(33)
Body      = version(1) ‖ Challenge ‖ Binding ‖ Notes ‖ Outputs ‖ TLV stream
            Challenge and Binding are length-prefixed; Notes are sorted by K and
            Outputs keep client order, both count-prefixed
Digest    = H_"bnap/spend"(Body)
SPEND     = Body ‖ one BIP-340 signature by each K over Digest, in Note order

Response  = status(1) ‖ ChangeSigs ‖ Ack
ChangeSig = C'(33) ‖ DLEQ(64), one per Output, in Output order
Ack       = BIP-340 signature by the op key over H_"bnap/ack"(Digest ‖ status ‖ amount ‖ ChangeSigs)
status    = 0 OK, 1 REPLAYED, 2 FAILED

The Digest covers the challenge, so it covers the scope, the price, the MAC and the recipient. The server checks a SPEND cheapest first:

  1. Parse. Check the version, the total size and the manifest’s max_notes and max_outputs.
  2. Replay lookup. If every K is already recorded with this Digest, return the stored Response with status REPLAYED, whatever the expiry or binding; the carrier MUST NOT perform the action again. If any K is recorded with a different Digest, fail with SPENT naming that K.
  3. Challenge. The MAC is valid and unexpired, or the self-issued fields are acceptable; the scope equals this request’s scope; the binding matches.
  4. Keysets. Every keyset is accepted, unexpired and not revoked, uses the challenge’s unit and holds each exponent used; no K or B’ appears twice.
  5. Amount. With checked arithmetic, the Notes minus the Outputs equal the amount exactly.
  6. Signatures. Every Note verifies (C = k_e·hash_to_curve(K)) and every signature by K covers the Digest.
  7. Commit. Record every K with the Digest and the Response in one atomic step, or record none.

Binding is empty, or the RFC 9266 tls-exporter value where the client can read it. Native clients and agents SHOULD bind; browsers cannot. A replayed SPEND is served from step 2 even on a new connection, so a lost response loses nothing.

The Ack is the receipt: one 64-byte operator signature that commits to what was paid, for which action, and what came back. Change is signed in the same response, so a deposit returned at once is simply change. A SPEND with amount 0 is a swap: it re-denominates notes or moves them to a new keyset.

Failure after payment. Operators SHOULD commit a spend together with its action. If the action fails after commit, the response carries status FAILED. The client redeems that SPEND body and response through ISSUE with value-in type 2, ack-refund, for new notes worth the amount. The issuer records every refunded Digest until the keyset expires, so each refund is paid once. Because FAILED is in the core, so is ack-refund.

ISSUE (client to issuer)

ISSUE    = version(1) ‖ value_in_type(BigSize) ‖ payload (length-prefixed) ‖ Outputs ‖ TLV stream
Response = status(1) ‖ ChangeSigs ‖ Ack over H_"bnap/issue"(ISSUE ‖ status ‖ ChangeSigs)

ISSUE is idempotent per payload: the same paid quote or refund claim always returns the same signatures. The value-in type is a registry number, like IP’s protocol field: the core names it and never parses it. Type 1, Lightning, is mandatory to implement; type 2, ack-refund, is part of the core; every other type is an extension.

Errors

A SPEND is atomic, so every error leaves every Note in the request unspent. Error responses SHOULD carry an operator signature over the Digest and the code, so a client can prove what the server claimed.

CodeMeaningClient action
VERSIONUnsupported versionUse one from the manifest
MALFORMEDBad encoding, or an unknown even TLVFix and resend
TOO_LARGEOver the size, note or output limitSwap into fewer notes first
UNKNOWN_KEYSETKeyset not accepted, expired or revokedRefresh the manifest, swap notes
UNIT_MISMATCHA Note or Output is in another unitUse the challenge’s unit
BAD_CHALLENGEMAC invalid, or self-issued challenges not allowedGet a challenge from the server
STALEChallenge expired, or self-issued challenge too oldGet a new challenge
WRONG_SCOPEChallenge or binding is for a different actionGet the right challenge
WRONG_AMOUNTNotes minus Outputs differ from the priceAdjust the change outputs
BAD_SIGNATUREA Note or a K signature failedDrop the bad Note
SPENTA K was spent in a different SPENDDrop that Note, resend the rest

Manifest

Published at /.well-known/bnap:

{
  "versions": [1],
  "keysets": [{ "id": "00ad…", "unit": "sat", "expiry": 1830297600, "revoked": false,
                "keys": { "0": "02…", "1": "03…" } }],
  "prices": { "table_version": 7, "POST /t/*/reply": 8, "POST /signup": 64 },
  "limits": { "max_notes": 64, "max_outputs": 64, "max_bytes": 16384 },
  "issue": "https://forum.example/bnap/issue",
  "value_in": [1, 2],
  "op_keys": { "k1": "02ab…" },
  "self_issued": { "allowed": true, "max_age_s": { "http": 60, "nostr": 86400 } }
}

State

BNAP keeps no per-client state. The server stores records about notes, never about people, as Bitcoin stores only the UTXO set:

  • a spend record per K (Digest and Response), kept until its keyset expires;
  • the set of refunded Digests, kept as long;
  • pending Lightning quotes;
  • its keys: issuer keysets, the MAC key and the operator keys.

Keysets. Keys rotate by publishing a new keyset; notes in an expiring keyset are swapped into the new one before expiry, and notes left unswapped are lost, so expiry SHOULD be at least a year away. An expired keyset ID MUST never be accepted again, because its spend records are gone. A compromised keyset is marked revoked and replaced at once. An operator with several points of presence either gives each its own keyset, keeping verification fully offline, or shares one atomic store of spend records.

Extensibility

The TLV stream at the end of a SPEND or ISSUE carries extensions. An unknown even type is rejected with MALFORMED; an unknown odd type is ignored. Every SPEND MUST carry exactly one GREASE record: a random type from the reserved odd range with a 4-byte random value, so no wallet can be told apart by it. The version, suite, value-in and TLV registries each reserve GREASE values.

HTTP binding (mandatory for HTTP servers)

HTTP+BNAP is one 402 response and one retry: the challenge rides in WWW-Authenticate next to any other payment scheme, and the spend rides in Authorization.

POST /t/123/reply HTTP/1.1
Host: forum.example
Content-Digest: sha-256=:X48E9q…:

HTTP/1.1 402 Payment Required
WWW-Authenticate: BNAP challenge="<base64url TLV>", amount="8", unit="sat", issue="/bnap/issue"
WWW-Authenticate: Payment id="…", method="lightning", intent="charge", request="…"

POST /t/123/reply HTTP/1.1
Host: forum.example
Content-Digest: sha-256=:X48E9q…:
Authorization: BNAP spend="<base64url SPEND>"

HTTP/1.1 201 Created
Authentication-Info: bnap="<base64url Response with Ack>"
  • Scope. scope = H_“bnap/scope/http”(method ‖ 0x00 ‖ absolute URI with scheme and authority ‖ 0x00 ‖ the Content-Digest value from RFC 9530, empty when there is no body). It binds the spend to the origin and to the exact content, so nobody on the path can reuse it for a different body.
  • Signed bytes. The challenge parameter carries the encoded challenge; amount, unit and issue are readable copies that MUST match it.
  • If Authorization already carries application auth, the client sends BNAP-Authorization instead.
  • Errors come back as 402 with BNAP-Error: STALE (or another code), its signature and a fresh challenge.
  • A client that cached the manifest MAY send a self-issued challenge on the first request and skip the 402, where the manifest allows it.
  • Sessions are ordinary cookies: a server MAY set one after a successful SPEND and decide when to charge again. The core has no session object.
  • Browser wallets send an empty binding; native clients and agents send the TLS exporter value, and a terminating CDN forwards it to the origin as RFC 9729 does.

Lightning issuance (mandatory to implement)

Value-in type 1 buys notes with one Lightning payment, in the quote, pay, issue pattern Cashu uses for minting (NUT-04). Every BNAP issuer MUST support it, so any two conforming implementations share at least one way to move value; this is how TLS mandates one cipher suite while allowing many. In IETF form it is a separate document the core references.

  1. Quote. The client POSTs {amount, unit, client_key} to the issue URI and receives a BOLT11 invoice and a quote id. The quote is locked to client_key, so a leaked quote id is worthless (as Cashu NUT-20 does).
  2. Pay. The client pays the invoice from any Lightning wallet, including a Cashu wallet melting ecash.
  3. Issue. The client sends ISSUE with ValueIn{1, quote_id}, its blinded outputs and a signature by client_key. The issuer checks that the invoice is paid and returns blind signatures with DLEQ proofs. Repeating the ISSUE returns the same signatures.

BOLT12 offers are RECOMMENDED wherever both sides support them. Withdrawal, turning notes back into a Lightning payment, is a separate extension, because some operators sell credit that is not refundable.

A typical human wallet buys a few thousand sats of notes per site in one payment and then spends them silently under a budget, so BNAP feels like a flat rate rather than a meter. Thirty years of failed micropayment schemes say this is the make-or-break interface rule.

Security and privacy considerations

Every threat the two debate rounds and the two independent reviews found is closed by a core rule; the residual risks are economic, legal or accepted on purpose.

ThreatClosed by
Double spendSpend record per K, committed atomically for a whole spend
One payment replayed for many actionsA known Digest returns the stored answer and the action MUST NOT run again
A spend reused with another body or at another originThe scope covers content and recipient, and the Digest covers the scope
Note theft from logs, proxies or CDNsK must sign; a copied Note is useless without k_n
Change theft by a proxyThe signature covers the outputs and the whole body
Paying a cheap challenge for a costly actionScope inside the challenge MAC
Value created by mixed units or overflowOne unit per spend; exponents checked before checked arithmetic
Admission minted by someone elseThe operator accepts only its own keysets
Issuer linking a spend to a purchaseBlinding; the DLEQ is never presented
Per-user tagging keysFull key lists cross-checked; keysets published 30 days ahead
Wallet fingerprintingPower-of-two notes; exactly one GREASE record in every spend
A refund claimed twiceRefunded Digests are recorded
A leaked Lightning quote idQuotes are locked to a client key
Lost responsesReplays served from the stored record, even after expiry
Junk spends as denial of serviceSize, limit and lookup checks before any elliptic-curve work
Tracking across sitesEach K is used at one origin, once
OssificationEven/odd TLV rule plus GREASE

Accepted residual risks:

  • Key compromise. A stolen issuer key forges notes that cannot be told apart, so the keyset is revoked and replaced at once. A stolen operator key can mint value through forged FAILED acknowledgements, so operator keys stay offline-capable and refunds are capped per epoch. A stolen MAC key forges cheap quotes, so MAC keys rotate often.
  • Small operators can still link visitors by IP address, TLS fingerprint and timing, including the gap between a Lightning purchase and the first spend and which notes are spent together. BNAP promises unlinkable notes, not anonymity.
  • Notes are operator credit. An operator that disappears takes unspent notes with it, so wallets keep per-site balances small, and notes left unswapped at keyset expiry are lost.
  • Unsigned errors. A server that skips the optional error signature can claim SPENT without proof.
  • Suite 0 is not post-quantum; the suite byte exists for that migration.
  • Unflagged abuse pays only the price; BNAP-B adds deposits where an operator can detect abuse.

Regulatory note. Each operator issues notes only for its own service, which reads closer to a merchant selling prepaid credit than to a custodian. From July 2027 the EU AMLR bars licensed crypto-asset providers from anonymous accounts. That reading needs EU counsel before anyone relies on it.

Extensions and roadmap

Nothing from v0.2 is lost; it moves to where it belongs. The core holds only what every implementation must share, and each other feature becomes its own short document or the operator’s own policy. The v0.2 archive tab holds the detailed drafts these documents start from.

Why bonds left the core. A deposit returned immediately is just change, which the core already does. A real deposit is change the server holds back and signs later unless it forfeits. That needs per-client state, a release step, reason codes and a transparency log: four mechanisms for an instrument that only bites when the operator can detect abuse. It composes cleanly on top as BNAP-B, with no new cryptography: a SPEND whose outputs are held, plus a release that returns their signatures.

DocumentWhat it addsStatus
BNAP coreThe Note, three verbs, ack-refund (value-in type 2)Mandatory
HTTP binding402 challenge, Authorization spend, content-covering scopeMandatory for HTTP servers
Value-in 1: LightningQuote locked to a client key, pay, issueMandatory for issuers
SASL+BNAPBNAP and BNAP-PLUS mechanisms for SMTP submission, IMAP, XMPP and MQTT 5First extension
BNAP-B: depositsHeld change with release or forfeit; operator-chosen forfeit destination, reason codes, forfeit log, claim rates; Ark and Cashu escrowExperimental
More value-in typesBOLT12, Ark, Cashu, hashcash, Privacy Pass attestation, operator grantsOptional, one registry entry each
WithdrawalNotes back to LightningOptional
Nostr NIP, ESMTP parameterStore-and-forward carriers using self-issued challengesOptional
DNS SVCB bnap hintSkip the first 402Optional
x402 scheme, MPP method, L402 bridgeInteroperability with other 402 schemesOptional
Pricing guidanceLoad-based prices, ceilings, fair queuing, a reserve for humansInformational
Wallet guidanceBudgets, top-ups, agent SDK, people without walletsInformational
TLS/QUIC admissionPricing handshake floods before any application runsDeferred until deployments exist
Sessions, linkable mode, per-request K-signaturesReplaced by cookies after a SPENDRemoved

Reference implementation: Caparra

Caparra is the Rust reference implementation of BNAP. Its core crate is small enough to audit in a day, and every extension lives in its own crate, so a solo maintainer never carries optional code on the main path.

CrateResponsibilityBuilds on
bnapNote, CHALLENGE, SPEND, ISSUE, Ack, TLV codec, test vectors; no_stdsecp256k1, sha2
caparra-issuerKeysets, blind signing, DLEQ, spent-set backendsCDK (Cashu Dev Kit) primitives
caparra-httpTower middleware and the caparra-gate reverse proxytower, hyper, rustls
caparra-lnValue-in type 1LDK
caparra-walletClient wallet, budgets, the bnap CLI, an MCP server for agentsAll of the above
caparra-sasl, caparra-b, …One crate per extensionbnap

Build order: bnap with test vectors, then caparra-issuer, then caparra-http and the gate, then caparra-ln, then the wallet and CLI, then extensions.

Targets to benchmark: one spend verified in under 1 ms per Note on one core, with no network call. These are design targets, not measurements.

Names checked: bnap is free on crates.io and npm; caparra is free on npm and still needs a manual check on crates.io.

Standardization

The first wave is three short documents, about 24 pages in total; everything else waits until two independent implementations interoperate. For scale, UDP (RFC 768) is 3 pages and IPv4 (RFC 791) is 45.

DocumentLengthVenueTarget
BNAP core~12 pagesCashu NUT, then IETF individual draftStandards Track candidate
HTTP binding~8 pagesIETF HTTP areaStandards Track candidate
Value-in 1: Lightning~4 pagesCashu NUTAccepted NUT
SASL+BNAP~6 pagesIETF, IANA SASL registryStandards Track candidate
BNAP-B deposits—Individual draftExperimental

Core document outline: terminology and model; Suite 0; the Note; CHALLENGE, SPEND and ISSUE; verification order and the spent-set; keysets and rotation; errors; extensibility and GREASE; security and privacy considerations; IANA considerations; test vectors.

IANA registrations: the BNAP HTTP authentication scheme; the BNAP-Error and BNAP-Authorization header fields; the bnap well-known URI; the SASL mechanisms; registries for suites, value-in types and TLV types, each with GREASE values reserved.

Licensing and governance. Code is MIT/Apache-2.0; drafts follow the IETF Trust terms. A single maintainer runs public proposals until two independent implementations exist, then an editors’ group takes over the spec. No maintainer ever holds funds, runs a default issuer or ships an allowlist.

Appendix A: Debate record

Round 2, 10 October: how small can the core be?

Four agents argued in an opening round and a rebuttal round: a minimalist (one primitive, IP-style), a core defender (what must stay or the system breaks), a stack architect (where BNAP sits) and a naming agent. The minimalist set the shape; the defender added the safety rules; both signed the result.

QuestionMinimalistCore defenderVerdict
The primitiveA blind-signed bearer noteMust be key-locked and extensibleNote = blind-signed public key K; K is the nullifier
What the spend signsChallenge and bindingThe entire spend, outputs includedThe entire body, like TLS Finished
VersioningVersion, suite and reserved bits in the NoteExtension map and GREASENo version field in the Note, one version byte per message; suite in the keyset ID; even/odd TLVs and GREASE
ReceiptsThe change is the receiptA signed receipt is mandatoryOne operator signature, the Ack
Failure and retriesApplication concernCore ruleAtomic, idempotent spends in core; refunds through ack-refund
Spends without a round tripNot allowedNeeded for Nostr and mailSelf-issued challenges, operator opt-in
Mandatory railNoneLightningLightning BOLT11
BondsDeferred change, an extensionOut of the coreBNAP-B, Experimental
TLS/QUIC bindingDeleteDeleteDeferred until deployments exist
Privacy PassShip as a token type?Fixed digest, no change channelNot a token type; usable as a value-in attestation

Stack architect. BNAP is not HTTPS-shaped, because it must see actions and prices; it belongs in authentication slots. The Bitcoin view, with payment on the rail and admission at the waist, is the framing that makes it foundational. Slash means over, plus means carried in.

Naming. About 50 candidates were checked against IETF protocols, Bitcoin projects and package registries. Eliminated for collisions: SATP (an IETF working group), BAP (Bitcoin Attestation Protocol), TAP (Taproot Assets), PCP, SAP, PAT (Privacy Pass), CNAP (3GPP caller ID), P402 (too close to L402 and x402). The finalists were BNAP and CPR (Cost Per Request); the owner chose BNAP.

Round 1, 9 October: custody, layer and economics

DecisionVerdict
CustodyThe operator issues its own notes; Bitcoin rails only fund them; no global mint allowlist
Economic corePer-action prices are primary; refundable bonds cannot deter abuse nobody flags
LayerApplication carriers first; transport admission later
ForfeitsOperator chooses the destination and must declare it; objective reason codes; public log
Relationship to x402Bridge, don’t compete
FramingSpam economics, not superintelligence

Independent review of v0.1 found five blockers, including the DLEQ linking leak and session ids usable as bearer tokens; v0.2 fixed all of them, and v0.3 removes the session objects entirely.

Independent review of the first v0.3 draft found three blockers: a retried SPEND could buy the action again, the HTTP scope covered neither the request body nor the origin, and the wire encoding was undefined. It also found seven major gaps: double-claimable refunds, unchecked units and overflow, a self-issued challenge with no published price, a grindable keyset ID, fingerprintable GREASE, missing key-compromise rules, and an overstated claim that the server keeps only one set. All are fixed in this version. The reviewer confirmed that using the Note key K as the blind-signature secret is sound.

Open questions

The core is small enough that the open questions are mostly about naming, law and adoption, not cryptography.

  • Name. BNAP is chosen; check the IANA service-name registry, the caparra crate and EU trademarks before the first public repository.
  • Bonds. Confirm BNAP-B as an Experimental extension rather than part of the core, which departs from the original choice of a refundable bond.
  • Carrier ages. Set max_age_s for self-issued challenges on Nostr and relayed mail.
  • Normative reference. Decide how the IETF core references the Lightning value-in document without a downward reference to a Cashu NUT.
  • Post-quantum suite. Pick a blind-signature scheme for a future suite.
  • Review venues. Take the HTTP binding to the IETF Privacy Pass or HTTP authentication communities for early review.
  • EU legal reading of operator-issued prepaid notes under MiCA and AMLR.
  • Wallets. Choose wallet partners for the browser extension; Lightning public capacity is about 4,900 BTC and Cashu has little consumer reach.
  • Test vectors and benchmarks. Publish vectors for every encoding and measure verification against the 1 ms target.

Sources

Opened during the debates:

Found by search but not opened:

Cited from the agents’ own knowledge and not verified in this session: Saltzer, Reed and Clark (1984); Clark (1988); Lightning BOLT 1; RFC 768; RFC 791; RFC 6454; RFC 9266. The v0.2 archive tab lists the round 1 sources.