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”.
| Written | Read as |
|---|---|
| BNAP/LN | BNAP notes funded over Lightning |
| BNAP/Ark, BNAP/Cashu | BNAP notes funded over Ark or Cashu ecash |
| HTTP+BNAP | HTTP carrying BNAP in its authentication slot |
| HTTP+BNAP/LN | The full suite: HTTP with BNAP, funded over Lightning |
| SASL+BNAP | SMTP, IMAP or XMPP carrying BNAP through SASL |
| BNAP-B | The 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.
| Actor | Outcome under BNAP |
|---|---|
| Slop and spam farms | Priced out once the price exceeds revenue per action |
| Sybil swarms | No gain from splitting; the cost is per action |
| Paying agents | Admitted at the same price as anyone |
| Capital-rich or state actors | Not deterred; out of scope |
| Volumetric floods | Out of scope |
| The issuing service | Cannot 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.
- 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).
- 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.
- 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.
- 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.
- 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.
- Sign everything once. Each note key signs a hash of the entire spend, as TLS Finished covers the whole transcript.
- 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:
| Layer | Contents | Responsible for |
|---|---|---|
| Policy | Prices, abuse judgement, deposits, sessions | Everything that needs application meaning |
| Carrier | HTTP authentication scheme, SASL, Nostr, ESMTP | Putting the bytes into a protocol slot |
| Admission | BNAP: the Note and three verbs | Proving issuer-scoped value was spent once, for this request |
| Value | Lightning, Ark, Cashu ecash, plus work and attested grants | Moving value, escrow, refunds |
| Settlement | Bitcoin | Final 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)
- The client generates a fresh key pair (k_n, K) and computes Y = hash_to_curve(K).
- It blinds Y as B’ = Y + r·G and sends B’ in an ISSUE or as a change output.
- 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).
- To spend, the client presents (Keyset ID, e, C, K) and signs the spend digest with k_n.
- 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.
| Type | Field | Meaning |
|---|---|---|
| 0 | version | Protocol version, 1 |
| 2 | scope | 32-byte hash of the action; each carrier defines it to cover the action’s content and its recipient |
| 4 | amount | The price |
| 6 | unit | Unit string, such as sat |
| 8 | keysets | Accepted keyset IDs, 8 bytes each |
| 10 | expires | Unix time after which the quote lapses |
| 12 | issue | URI of the ISSUE endpoint |
| 14 | op_key_id | Which operator key signs acknowledgements |
| 16 | nonce | 16 random bytes |
| 18 | mac | Server MAC over every record above |
| 20 | issued_at | Self-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:
- Parse. Check the version, the total size and the manifest’s
max_notesandmax_outputs. - 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
SPENTnaming that K. - Challenge. The MAC is valid and unexpired, or the self-issued fields are acceptable; the scope equals this request’s scope; the binding matches.
- 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.
- Amount. With checked arithmetic, the Notes minus the Outputs equal the amount exactly.
- Signatures. Every Note verifies (C = k_e·hash_to_curve(K)) and every signature by K covers the Digest.
- 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.
| Code | Meaning | Client action |
|---|---|---|
VERSION | Unsupported version | Use one from the manifest |
MALFORMED | Bad encoding, or an unknown even TLV | Fix and resend |
TOO_LARGE | Over the size, note or output limit | Swap into fewer notes first |
UNKNOWN_KEYSET | Keyset not accepted, expired or revoked | Refresh the manifest, swap notes |
UNIT_MISMATCH | A Note or Output is in another unit | Use the challenge’s unit |
BAD_CHALLENGE | MAC invalid, or self-issued challenges not allowed | Get a challenge from the server |
STALE | Challenge expired, or self-issued challenge too old | Get a new challenge |
WRONG_SCOPE | Challenge or binding is for a different action | Get the right challenge |
WRONG_AMOUNT | Notes minus Outputs differ from the price | Adjust the change outputs |
BAD_SIGNATURE | A Note or a K signature failed | Drop the bad Note |
SPENT | A K was spent in a different SPEND | Drop 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
challengeparameter carries the encoded challenge;amount,unitandissueare readable copies that MUST match it. - If
Authorizationalready carries application auth, the client sendsBNAP-Authorizationinstead. - 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.
- 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 toclient_key, so a leaked quote id is worthless (as Cashu NUT-20 does). - Pay. The client pays the invoice from any Lightning wallet, including a Cashu wallet melting ecash.
- Issue. The client sends ISSUE with
ValueIn{1, quote_id}, its blinded outputs and a signature byclient_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.
| Threat | Closed by |
|---|---|
| Double spend | Spend record per K, committed atomically for a whole spend |
| One payment replayed for many actions | A known Digest returns the stored answer and the action MUST NOT run again |
| A spend reused with another body or at another origin | The scope covers content and recipient, and the Digest covers the scope |
| Note theft from logs, proxies or CDNs | K must sign; a copied Note is useless without k_n |
| Change theft by a proxy | The signature covers the outputs and the whole body |
| Paying a cheap challenge for a costly action | Scope inside the challenge MAC |
| Value created by mixed units or overflow | One unit per spend; exponents checked before checked arithmetic |
| Admission minted by someone else | The operator accepts only its own keysets |
| Issuer linking a spend to a purchase | Blinding; the DLEQ is never presented |
| Per-user tagging keys | Full key lists cross-checked; keysets published 30 days ahead |
| Wallet fingerprinting | Power-of-two notes; exactly one GREASE record in every spend |
| A refund claimed twice | Refunded Digests are recorded |
| A leaked Lightning quote id | Quotes are locked to a client key |
| Lost responses | Replays served from the stored record, even after expiry |
| Junk spends as denial of service | Size, limit and lookup checks before any elliptic-curve work |
| Tracking across sites | Each K is used at one origin, once |
| Ossification | Even/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.
| Document | What it adds | Status |
|---|---|---|
| BNAP core | The Note, three verbs, ack-refund (value-in type 2) | Mandatory |
| HTTP binding | 402 challenge, Authorization spend, content-covering scope | Mandatory for HTTP servers |
| Value-in 1: Lightning | Quote locked to a client key, pay, issue | Mandatory for issuers |
| SASL+BNAP | BNAP and BNAP-PLUS mechanisms for SMTP submission, IMAP, XMPP and MQTT 5 | First extension |
| BNAP-B: deposits | Held change with release or forfeit; operator-chosen forfeit destination, reason codes, forfeit log, claim rates; Ark and Cashu escrow | Experimental |
| More value-in types | BOLT12, Ark, Cashu, hashcash, Privacy Pass attestation, operator grants | Optional, one registry entry each |
| Withdrawal | Notes back to Lightning | Optional |
| Nostr NIP, ESMTP parameter | Store-and-forward carriers using self-issued challenges | Optional |
DNS SVCB bnap hint | Skip the first 402 | Optional |
| x402 scheme, MPP method, L402 bridge | Interoperability with other 402 schemes | Optional |
| Pricing guidance | Load-based prices, ceilings, fair queuing, a reserve for humans | Informational |
| Wallet guidance | Budgets, top-ups, agent SDK, people without wallets | Informational |
| TLS/QUIC admission | Pricing handshake floods before any application runs | Deferred until deployments exist |
| Sessions, linkable mode, per-request K-signatures | Replaced by cookies after a SPEND | Removed |
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.
| Crate | Responsibility | Builds on |
|---|---|---|
bnap | Note, CHALLENGE, SPEND, ISSUE, Ack, TLV codec, test vectors; no_std | secp256k1, sha2 |
caparra-issuer | Keysets, blind signing, DLEQ, spent-set backends | CDK (Cashu Dev Kit) primitives |
caparra-http | Tower middleware and the caparra-gate reverse proxy | tower, hyper, rustls |
caparra-ln | Value-in type 1 | LDK |
caparra-wallet | Client wallet, budgets, the bnap CLI, an MCP server for agents | All of the above |
caparra-sasl, caparra-b, … | One crate per extension | bnap |
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.
| Document | Length | Venue | Target |
|---|---|---|---|
| BNAP core | ~12 pages | Cashu NUT, then IETF individual draft | Standards Track candidate |
| HTTP binding | ~8 pages | IETF HTTP area | Standards Track candidate |
| Value-in 1: Lightning | ~4 pages | Cashu NUT | Accepted NUT |
| SASL+BNAP | ~6 pages | IETF, IANA SASL registry | Standards Track candidate |
| BNAP-B deposits | — | Individual draft | Experimental |
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.
| Question | Minimalist | Core defender | Verdict |
|---|---|---|---|
| The primitive | A blind-signed bearer note | Must be key-locked and extensible | Note = blind-signed public key K; K is the nullifier |
| What the spend signs | Challenge and binding | The entire spend, outputs included | The entire body, like TLS Finished |
| Versioning | Version, suite and reserved bits in the Note | Extension map and GREASE | No version field in the Note, one version byte per message; suite in the keyset ID; even/odd TLVs and GREASE |
| Receipts | The change is the receipt | A signed receipt is mandatory | One operator signature, the Ack |
| Failure and retries | Application concern | Core rule | Atomic, idempotent spends in core; refunds through ack-refund |
| Spends without a round trip | Not allowed | Needed for Nostr and mail | Self-issued challenges, operator opt-in |
| Mandatory rail | None | Lightning | Lightning BOLT11 |
| Bonds | Deferred change, an extension | Out of the core | BNAP-B, Experimental |
| TLS/QUIC binding | Delete | Delete | Deferred until deployments exist |
| Privacy Pass | Ship as a token type? | Fixed digest, no change channel | Not 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
| Decision | Verdict |
|---|---|
| Custody | The operator issues its own notes; Bitcoin rails only fund them; no global mint allowlist |
| Economic core | Per-action prices are primary; refundable bonds cannot deter abuse nobody flags |
| Layer | Application carriers first; transport admission later |
| Forfeits | Operator chooses the destination and must declare it; objective reason codes; public log |
| Relationship to x402 | Bridge, don’t compete |
| Framing | Spam 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
caparracrate 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_sfor 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:
- RFC 9577: Privacy Pass HTTP authentication scheme
- Cashu NUT-24: HTTP 402
- Cashu NUT specifications
- IETF SATP working group
- RFC 9729: Concealed HTTP authentication
- draft-httpauth-payment-01
- IACR ePrint 2026/2174: Cashu system-level analysis
- Laurie and Clayton: Proof-of-work proves not to work
- rustls feature manual
Found by search but not opened:
- RFC 9170: Long-term viability of protocol extension mechanisms
- RFC 3439: Some Internet architectural guidelines
- Beck: On the hourglass model
- Akhshabi and Dovrolis: EvoArch
- RFC 9576: Privacy Pass architecture
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.