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: an admission layer for priced actions — Draft Specification v0.4

2026-10-10 · Dominic

Since v0.5 this tab is the detailed specification of one credential scheme, bnap:notes, the default scheme. Where it differs from the v0.5 socket, v0.5 wins: its CHALLENGE is the socket’s Offer, its witnesses sign the socket’s Digest, and commit, replay and receipts are owned by the socket.

Abstract and status

BNAP lets an online service put a small price, in sats, on an action such as an API call, a post or a signup, and check payment offline without learning who paid. It is a Cashu profile: the service runs its own Cashu-style issuer, sells prepaid credit over Lightning, and accepts that credit back as Notes, each bound to one specific request. BNAP aims to be a small, stable admission layer that many carriers and value rails can share without changes to the core.

Terminology. BNAP (“bee-nap”, Blind-Note Admission Protocol) is the protocol; Caparra is its Rust reference implementation. A slash means “funded over”, with the lower layer on the right; a plus means “carried in”.

WrittenRead as
BNAP/LNBNAP credit funded over Lightning
BNAP/ArkBNAP credit funded over Ark (experimental)
HTTP+BNAPHTTP carrying BNAP in its authentication slot
HTTP+BNAP/LNHTTP with BNAP, funded over Lightning
SASL+BNAPMail, chat or MQTT carrying BNAP through SASL
BNAP-W, BNAP-BThe withdrawal and deposit extensions

Status. Pre-draft, version 0.4. A third expert round checked v0.3 against what Cashu, Lightning and the IETF have shipped by October 2026: an ecash cryptographer, an anonymous-credentials expert, a Lightning engineer and a Bitcoin protocol reviewer. Earlier versions stay in the archive tabs. Key words follow BCP 14 (RFC 2119, RFC 8174).

Owner decisionChoice
NameBNAP
FramingA layered spec with modest framing; the TCP/IP analogies live in Appendix A
EcosystemA Cashu profile: Notes are valid Cashu proofs, and buying credit is Cashu’s Lightning mint flow
WithdrawalAn optional extension, BNAP-W, off by default
First deploymentAI agents and paid APIs

What round 3 changed.

  • Notes reuse Cashu verbatim: the secret is the hex of the note key, keyset IDs are Cashu’s version-2 IDs, and DLEQ proofs follow NUT-12.
  • Wallets recover from a seed (NUT-13 derivation); in v0.3 a lost device meant lost funds.
  • Credit is bought through Cashu’s Lightning mint flow (NUT-23, NUT-04, NUT-20), and a first-time agent can fund and act in one round trip.
  • Refunds by operator signature (ack-refund) are gone: a failed action returns refund outputs the client supplied, signed in the same response.
  • A spend costs about 300 bytes on disk, including the signed outputs Cashu restore needs, and replies to retries are recomputed rather than stored.
  • Agents with no BNAP code can still pay per action through L402 or MPP challenges in the same 402 response.

Why BNAP exists

BNAP removes actions that are only profitable at a price of zero: agents hammering free APIs, scraping, slop posts, spam and bulk signups. An automated action stops paying the moment its price exceeds its value to the actor:

p > v_{action}

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

In plain terms, BNAP is prepaid site credit, priced in sats, issued and redeemed by one site, never transferable between sites, and withdrawable only where the operator opts in. Each spend is bound to one request and cannot be linked to the purchase that funded it.

Existing schemeWhat it doesWhat BNAP adds
L402 (Lightning Labs)One Lightning payment per request, proved by preimage and macaroonPay once, spend offline many times; spends unlinkable to the payment
x402, MPPStablecoin or card payment per requestBitcoin-native credit with no facilitator, unlinkable spends
Cashu NUT-22 blind authSingle-use ecash tokens for accessA price, change, receipts, and theft resistance by binding each note to its request
Cashu NUT-24HTTP 402 paid with ecashA self-issued mint, request binding and receipts; NUT-24 uses an X-Cashu header with NUT-18 payment requests and lists no implementations yet
Privacy Pass (RFC 9576–9578)Unlinkable tokens from an attesterValue and change, funded by payment instead of attestation

Non-goals: proving personhood; stopping well-funded attackers or volumetric floods; replacing the operator’s abuse classifier; being a token or a payment network.

Design principles

Eight principles decide every trade-off; the first is new in v0.4.

  1. Reuse before inventing. Every object Cashu, Lightning or the IETF already defines is used verbatim. BNAP adds only the request binding, the price rules and the receipt.
  2. One small core. Many value rails below, many carriers above, one object in the middle: the Note.
  3. End to end. Abuse is judged by the application; BNAP only makes a price verifiable.
  4. No per-client state. The server stores records about notes, never about people.
  5. Pay rarely, spend offline. Money moves when credit is bought; each action is a local check.
  6. The relying party is its own issuer. A service accepts only notes it signed, so there are no allowlists and no fake mints.
  7. Sign everything once. Each note key signs a hash of the whole spend, outputs included.
  8. It’s OK to be odd. An unknown even extension is rejected, an unknown odd one is ignored, and clients send GREASE so the rule never rusts.

Architecture (informative)

BNAP admits one action at one origin when 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: HTTP+BNAP/LN agent flow · tier 1. The source tab shows this as an interactive diagram (widget node/83a7fbbc-e1ed); it is not part of this export. See the source doc.

Only the first request touches Lightning, and only it is linkable to the payment; every later request is a local check at the gateway.

BNAP lives in the authentication slot of existing protocols, as SASL mechanisms and HTTP authentication schemes do, because it must know which request is an action and what it costs. It needs no new URI scheme and no TLS change.

LayerContentsResponsible for
PolicyPrices, abuse judgement, sessions, depositsEverything that needs application meaning
CarrierHTTP authentication scheme (first), SASL, NostrPutting the bytes into a protocol slot
AdmissionBNAP: Notes and three verbsProving issuer-scoped value was spent once, for this request
ValueLightning through Cashu mint quotes; Ark and Cashu tokens experimentalMoving value
SettlementBitcoinFinal settlement

What BNAP does not do:

  • A valid spend is necessary, not sufficient; the operator may still queue or refuse.
  • It moves no money; 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.

Notes: the Cashu profile

A Note is a Cashu proof whose secret is the hex encoding of a public key K, so only the holder of K’s private key can spend it. Everything else about the Note is Cashu as the NUTs specify it.

k_n, r   = NUT-13 version-2 derivation (seed, full keyset ID, counter, type 0x02)
K        = x-only public key of k_n (BIP-340)
secret   = lowercase hex of K, 64 characters
Y        = hash_to_curve(UTF-8 secret)              NUT-00
B'       = Y + r·G                                   sent at purchase or as change
C'       = k_a·B' with a NUT-12 DLEQ proof           returned by the issuer
C        = C' − r·A                                  unblinded signature
Proof    = {id, amount, secret, C}                   a valid Cashu proof
witness  = BIP-340 signature by k_n over the spend Digest

Cashu pieces reused verbatim

PieceNUTUse in BNAP
Blind signatures and hash_to_curve00Note math and test vectors
Keysets, version-2 IDs, final expiry, input fees01, 02Keyset identity, rotation and fees
DLEQ proofs with deterministic nonce12Purchase and change proofs; the deterministic nonce is a MUST in BNAP
Deterministic secrets13Note keys, blinding factors, recovery
Restore and state check09, 07Recovery and multi-device checks
Lightning mint quotes with signed locks04, 20, 23, 25Buying credit
Info endpoint06Discovery and prices
V4 token (cashuB)00Wallet storage format; notes are not transferable, since spending also needs K’s private key

Rules beyond Cashu

  • Spends need the key. A BNAP issuer MUST accept its notes only through a BNAP SPEND with a valid witness, never through a plain Cashu swap or melt, because hex(K) would otherwise read as an unlocked secret.
  • Key isolation. BNAP keysets MUST NOT be served by a mint that allows plain swaps or melts: NUT-03 swap and NUT-05 melt are disabled for them (BNAP-W melts carry witnesses), and BNAP keys never appear in any other Cashu keyset.
  • Keyset IDs. The wire may carry the 8-byte short ID; the Digest and all derivation use the full version-2 ID, and an ambiguous short ID is rejected.
  • Outputs. Every B’ must be a valid point other than infinity, and a B’ signed before is rejected unless a retry reproduces the original response.
  • No signature aggregation. One BIP-340 signature per K; batch verification is fine. Purchase never proves knowledge of K’s private key, so a summed key could steal someone else’s note.
  • Consistent keys. Operators publish a keyset at least 30 days before issuing from it, and clients compare full key lists from a second vantage point to catch per-user tagging keys. A standby keyset is always pre-published, so a compromised keyset can be replaced at once.
  • Denominations. Amounts are powers of two; each K is used at one origin, once.

Recovery

A wallet holds one BIP-39 seed. Note keys and blinding factors come from NUT-13 version-2 derivation with derivation type 0x02 (to be requested from the Cashu maintainers), and the high 16 bits of the counter name a device or agent slot, so two devices never collide. A wallet restores by asking the issuer to re-sign its blinded outputs (NUT-09) and checking which notes are spent (NUT-07); because every priced spend leaves unsigned refund outputs, restore scans each device slot recorded in the backup until 3 × max_outputs consecutive counters come back unsigned. Issuers MUST keep every signed blinded output with its signature until the keyset’s final expiry, as NUT-09 requires. The wallet’s list of sites is backed up encrypted, as Cashu wallets back up their mint lists.

Suites

Inputs carry a suite tag. Suite 0 is this Cashu profile and is mandatory. Three suites are reserved, each defining its own value rules:

SuiteConstructionWhy
1RFC 9497 VOPRF with RFC 9380 hash-to-curveA form the IETF can standardize
2Anonymous Credit Tokens (ACT)One credential holds a hidden balance, removing power-of-two change
3Cashu KVAC researchAmount-hiding ecash

The protocol: three verbs

CHALLENGE and SPEND run on every priced action; ISSUE runs rarely and is Cashu’s mint flow.

Encoding

Objects travel as CBOR, the encoding of Cashu’s V4 tokens, and as base64url on text carriers. The bytes that get signed are a deterministic binary SigningBody built from those objects: big-endian integers, BigSize lengths and counts, and TLV streams in strictly ascending type order with no duplicates (Lightning BOLT 1). 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 that the server MACs, 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, in the keyset unit
6unitUnit string, normally sat
8keysetsAccepted keyset IDs
10expiresUnix time after which the quote lapses
12infoURI of the issuer’s info endpoint
14op_key_idWhich operator key signs acknowledgements
16nonce16 random bytes
18macServer MAC over every other record
20issued_atSelf-issued challenges only: creation time
22fund_quoteOptional: a NUT-23 mint quote id the client asked for

Self-issued challenges. Carriers without a round trip, such as Nostr events, let a client build a challenge itself with version, scope, amount, unit, keysets, its own nonce and issued_at, and no mac, where the manifest allows it. The operator accepts it if amount is at least its published price and the age is within that carrier’s max_age_s.

SPEND (client to server)

SPEND          = {v, challenge, binding, inputs, outputs, refund_outputs, fund?, ext}   (CBOR)
input          = {suite, s_id, amount, secret, C, witness}   a Note with its witness
output         = {s_id, amount, B'}                          a Cashu blinded message
fund           = {quote, signature}                          a paid NUT-23 quote
s_id           = first 8 bytes of the version-2 keyset ID, as in Cashu V4 tokens

Digest         = H_"bnap/spend"(SigningBody)                 byte layout below
witness        = BIP-340 signature by each K over Digest
fund.signature = BIP-340 signature over Digest by the quote's NUT-20 key (a BNAP use of that key)

Response       = {status, replayed, signatures, ack}
status         = OK or FAILED; replayed = true when answering a retry
signatures     = C' and NUT-12 DLEQ proof for each signed output, in order
ack            = BIP-340 signature by the operator key over
                 H_"bnap/ack"(Digest ‖ status ‖ amount ‖ signatures)
SigningBody fieldBytes
version1
challengeBigSize length, then the challenge TLV bytes as received
bindingBigSize length (0 or 32), then the bytes
inputsBigSize count; per input: suite (1) ‖ full keyset ID (33) ‖ amount (8, big-endian) ‖ K (32) ‖ C (33, compressed); sorted by K
outputsBigSize count; per output: full keyset ID (33) ‖ amount (8) ‖ B’ (33); in client order
refund_outputsSame layout as outputs
fundBigSize length, then the UTF-8 quote id; length 0 when absent
extTLV stream built from the ext map, in ascending type order

Amount rule. Inputs + paid quote − input fees − outputs = amount, exactly. Input fees follow the keysets’ input_fee_ppk (NUT-02); a fund quote pays no input fee. Whenever amount > 0, refund_outputs MUST be present and sum to exactly amount, so its presence never fingerprints a wallet. A SPEND MUST contain at least one input or a fund, and max_outputs counts outputs and refund outputs together.

The server checks a SPEND cheapest first:

  1. Parse. Version, total size, max_inputs and max_outputs; at least one input or a fund.
  2. Replay lookup. If every input’s Y, and the quote if any, is recorded with this Digest’s tag and the record is inside the manifest’s replay_window_s: re-verify every witness over the full Digest, then answer with the recorded status and replayed = true, or with PENDING if the action is still running. The carrier MUST NOT perform the action again. Replays are rate-limited per client network. If any Y is recorded with a different tag, or the window has passed, fail with SPENT.
  3. Challenge. The MAC is valid and unexpired, or the self-issued fields are acceptable; the scope and the binding match this request.
  4. Keysets and outputs. Every keyset is known, inside its final expiry and in the challenge’s unit; every amount exists in its keyset; no K or B’ repeats; no B’ was signed before (OUTPUT_REUSED). Outputs and refund outputs use keysets that are still issuing.
  5. Amount. The amount rule holds, with checked arithmetic.
  6. Signatures. Every Note satisfies C = k·hash_to_curve(secret); every witness covers the Digest; a fund quote is paid, not yet issued, and its key signed the Digest.
  7. Commit. In one atomic step, record every Y and the quote with the Digest tag and status PENDING, and mark the quote ISSUED so NUT-04 refuses it. Then run the action.

Outcomes.

  • OK: the server moves PENDING to OK and signs outputs, the change.
  • FAILED: the action failed after commit because of a server-side fault. The server moves PENDING to FAILED and signs outputs and refund_outputs, returning inputs + quote − fees in full. Servers MUST fully validate an action before commit, so FAILED is reserved for their own faults.
  • Errors before commit spend nothing, as in Cashu NUT-22.

The status changes exactly once, from PENDING to OK or FAILED, so a replay can never flip the outcome. Signing is deterministic: BNAP makes NUT-12’s deterministic DLEQ nonce a MUST, and BIP-340 signatures use zero auxiliary randomness, so replies are recomputed instead of stored. The ack is the receipt: one 64-byte operator signature over what was paid, for which action, and what came back. Binding is empty, or the RFC 9266 tls-exporter value; agents and native clients MUST bind, while browsers cannot.

Linkability. A SPEND that carries fund links this one action to the Lightning payment; every note issued as change is unlinkable. Wallets that need full unlinkability buy credit in a separate ISSUE first.

ISSUE, swap and restore

  • ISSUE is Cashu’s mint flow: a NUT-23 BOLT11 quote locked to a client key (NUT-20), paid, then minted with NUT-04; BOLT12 through NUT-25 is optional. A paid quote can instead be redeemed inside a SPEND as fund; the commit marks it ISSUED, and NUT-04 refuses it afterwards. If that SPEND fails before commit, the quote stays mintable through NUT-04.
  • Swap. Re-denominating notes is a SPEND with amount 0 sent to the info endpoint’s swap URL, with scope = H_“bnap/scope/swap”(origin). A plain Cashu swap is never accepted, because spends need witnesses.
  • Restore uses NUT-09, and state checks use NUT-07, both keyed by Y.

Errors

CodeMeaningClient action
VERSIONUnsupported versionUse one from the manifest
MALFORMEDBad encoding, an unknown even TLV, or no input and no fundFix and resend
TOO_LARGEOver a size, input or output limitSwap into fewer notes first
UNKNOWN_KEYSETKeyset unknown, expired or revokedRefresh keysets, swap notes
UNIT_MISMATCHA note or output is in another unitUse the challenge’s unit
BAD_CHALLENGEMAC invalid, or self-issued not allowedGet a challenge from the server
STALEChallenge expired or too oldGet a new challenge
WRONG_SCOPEChallenge or binding is for another actionGet the right challenge
WRONG_AMOUNTThe amount rule failsFix change or refund outputs
BAD_SIGNATUREA note, witness or quote signature failedDrop the bad input
OUTPUT_REUSEDA blinded output was signed beforeDerive fresh outputs
SPENTA note was spent in another SPEND, or the replay window passedDrop that note, resend the rest
QUOTE_UNPAIDThe fund quote is not paid, or already issuedPay it, or drop it
PENDINGThe original spend is still runningRetry after Retry-After
RATE_LIMITEDToo many replays or open quotesRetry later

Error responses SHOULD carry an operator signature over the Digest and the code.

Discovery

The issuer’s Cashu info endpoint (NUT-06) carries a bnap section, and /.well-known/bnap points to it. Keysets come from the standard Cashu keyset endpoints.

"bnap": {
  "versions": [1],
  "prices": { "table_version": 7,
              "POST /v1/chat": { "sat": 21, "ref": "EUR 0.012" },
              "POST /v1/search": { "sat": 4 } },
  "limits": { "max_inputs": 16, "max_outputs": 16, "max_bytes": 5000 },
  "max_fund": 10000,
  "replay_window_s": 86400,
  "op_keys": { "k1": "02ab…" },
  "self_issued": { "allowed": false },
  "swap": "/v1/bnap/swap",
  "withdrawal": false
}

A 5,000-byte SPEND is about 6.7 KB in base64url, under common 8 KB header limits.

State and storage

BNAP keeps no per-client state. For each spent note the server stores a 16-byte prefix of Y with a 16-byte Digest tag and a status byte, and the same for redeemed quotes. As NUT-09 requires, it also stores every signed blinded output with its signature, which enforces the no-reuse rule as well. A typical spend with two change outputs takes about 300 bytes on disk.

DeploymentSpends per yearStorage per year
Mid-size API, 20,000 spends a day7.3 millionabout 2.2 GB
Large service, 5 million spends a day1.8 billionabout 550 GB

Keysets issue for about 3 months and expire about 15 months after they start, so storage is bounded by spend rate times 15 months. A spend record is deleted when its keyset expires, and an expired keyset is never accepted again. If the spend store is lost, notes spent since the last durable copy can be spent again until expiry; without withdrawal that costs only free actions, never bitcoin. The issuer key lives in a separate signer process, derived from a cold seed, and a standby keyset is always pre-published so a compromised keyset can be revoked and replaced at once.

Extensibility

The TLV stream at the end of a SPEND carries extensions. An unknown even type is rejected with MALFORMED; an unknown odd type is ignored. Clients SHOULD send one GREASE record per SPEND, a random type from the reserved odd range with a 4-byte random value; servers MUST ignore it and MUST NOT require it. The version, suite and TLV registries each reserve GREASE values.

HTTP binding and the agent profile

The first deployment is AI agents and paid APIs: a gateway in front of an API, and a client any agent can call. One 402 response offers BNAP next to per-action Lightning schemes, so agents with no BNAP code can still pay.

POST /v1/chat HTTP/1.1
Host: api.example
Content-Digest: sha-256=:X48E9q…:
BNAP-Fund: amount=2100, pubkey=02c4…

HTTP/1.1 402 Payment Required
WWW-Authenticate: BNAP challenge="<base64url TLV>", amount="21", unit="sat",
    fund-quote="<base64url NUT-23 quote>", info="/v1/info"
WWW-Authenticate: L402 macaroon="…", invoice="lnbc210n1…"
WWW-Authenticate: Payment id="…", method="lightning", intent="charge", request="…"

(the agent checks the quote's key and amount, saves it, then pays through NWC)

POST /v1/chat HTTP/1.1
Host: api.example
Content-Digest: sha-256=:X48E9q…:
Authorization: BNAP <base64url SPEND with fund, outputs, refund_outputs>

HTTP/1.1 200 OK
Authentication-Info: bnap="<base64url Response>"

Rules

  • Scope. scope = H_“bnap/scope/http”(method ‖ 0x00 ‖ absolute URI normalized per RFC 3986 ‖ 0x00 ‖ the raw 32-byte SHA-256 digest from Content-Digest, empty when there is no body). A spend therefore names its origin and its exact content.
  • Funding is opt-in and capped. The server embeds a NUT-23 quote only when the request carries BNAP-Fund with an amount and a client key, never above the manifest’s max_fund, and rate-limits open quotes per client network. A scraper gets a challenge, never an invoice.
  • Zero-code agents. An HTTP+BNAP server SHOULD also offer an L402 challenge or an MPP Lightning charge challenge for one action at the same price. The macaroon or MPP request MUST carry the scope hash, payment hashes are kept in their own namespace until the invoice expires, and a request carrying more than one payment scheme is rejected. Unlike common L402 use, these tokens admit exactly one action.
  • Credentials. The SPEND rides as token68 after BNAP. If Authorization already carries application auth, the client sends BNAP-Authorization instead. amount, unit and info in the challenge are readable copies that MUST match the encoded challenge.
  • Status codes. OK returns the action’s own status. FAILED returns 502 with all signatures in Authentication-Info. PENDING returns 409 with Retry-After. A replay returns the stored action result if the gateway cached it, which it SHOULD for the replay window; otherwise 200 with an empty body and Authentication-Info.
  • Errors come back as 402 with BNAP-Error and a fresh challenge.
  • Sessions are ordinary cookies a server MAY set after a successful SPEND.
  • Binding. Agents and native clients MUST bind to the TLS exporter; browsers cannot and send an empty binding.

Agent tiers

TierThe agent hasHow it paysWhat links to the payment
0Any L402 or MPP client, such as lngetLightning per actionEvery action
1The bnap CLI or MCP server and an NWC wallet connectionFunds once inside the 402, then spends notes offlineOnly the first action
2A full wallet with its own seedBuys credit in a separate ISSUE, then spends notesNothing

Agent wallets MUST:

  • enforce spending caps per origin and per task in code, not in the model’s prompt;
  • use a fresh key per quote, check that the quote’s key and invoice amount match what they sent, and never pay more than their BNAP-Fund amount;
  • save the quote and its key before paying, so a failed SPEND can still mint the paid quote through NUT-04;
  • keep per-site balances small (default cap 5,000 sat), because credit is the operator’s IOU.

Tools. bnap get <url> works like curl: it answers 402s, funds over NWC within budget, spends notes and retries. The MCP server exposes fetch_with_bnap and budget to any agent framework.

Value layer: buying credit

Buying credit is Cashu’s Lightning mint flow, used verbatim; BNAP defines no payment protocol of its own.

  1. Quote. The client asks for a NUT-23 quote locked to its key (NUT-20), or receives one inside a 402.
  2. Pay. Any Lightning wallet pays the BOLT11 invoice, including Cashu, Ark, Spark and Fedimint wallets, which can all pay BOLT11.
  3. Mint. The client mints notes with NUT-04, or redeems the paid quote inside its next SPEND as fund.

BOLT11 is mandatory because the issuer is always online and every payer stack accepts it, including NWC, lnget, MPP and Cashu melt. BOLT12 through NUT-25 is optional, because not every Lightning implementation supports offers natively yet.

Operator receive backends

The wire fixes only BOLT11; the backend is the operator’s choice. Caparra defines one interface, InvoiceSource { create, status }.

BackendCustodyEffortFit
Receive-only NWC connection (Alby Hub or any NWC wallet)Depends on the walletPaste one connection stringCaparra default; it can never be drained
LDK Node with an LSPSelf-custodialRun a node libraryThe sovereign tier
PhoenixdSelf-custodialOne daemon; liquidity fees on the first receivesSmall operators
Custodial APICustodialEasiestBrings KYC

Other value-in types. Ark VTXOs (bark has been on mainnet since June 2026) and Cashu tokens from operator-listed mints are experimental. Hashcash is dropped: proof-of-work favours attackers with GPUs.

Units. Notes are in sat. Prices may carry a fiat reference that the operator re-converts at each price-table version; the challenge amount is always in sat. The smallest top-up is max(4 × price, 21 sat), because routing a few hundred sats costs almost nothing while the operator’s inbound liquidity is the real cost.

Withdrawal extension (BNAP-W)

Withdrawal is optional and off by default. Without it, a stolen issuer key or a lost spend store yields only free actions, never bitcoin; with it, both become theft. An operator that enables BNAP-W MUST:

  • write spend records synchronously to durable, replicated storage before answering;
  • cap withdrawals per epoch and per request;
  • pay out through a melt (NUT-05) that carries BNAP witnesses, to a Lightning invoice;
  • keep the issuer key in a separate signer process.

Paying out to arbitrary invoices moves an operator from merchant toward money transmitter, so BNAP-W needs legal review in each jurisdiction.

Without BNAP-W, wallets cap each site’s balance, and operators announce shutdown or keyset retirement at least 30 days ahead through the keyset’s final expiry. Refunds at shutdown are a consumer-law matter outside the protocol.

Security and privacy considerations

Each threat found across three debate rounds and two reviews maps to one rule above; what remains is economic, legal or accepted on purpose.

ThreatClosed by
Double spendEach Y recorded atomically before the action runs
One payment replayed for many actionsA known Digest returns the recorded status; the action never runs again
Replays as denial of serviceA replay window, rate limits, and at least one input or fund per SPEND
A spend reused with another body or at another originThe scope covers content and origin; the Digest covers the scope
Note theft from logs, proxies or CDNsA witness by K is required
Change or refund theft by a proxyThe Digest covers outputs, refund outputs and the quote
A fund quote redeemed twiceThe commit marks it ISSUED; NUT-04 refuses it afterwards
A quote swapped by a proxyWallets check the quote’s key and amount before paying
Forged replay through a tag collision16-byte tags, and full re-verification before any replay
One payment admitting two actions across schemesScope inside the macaroon or MPP request; one scheme per request
Theft with a summed keyOne signature per K, no aggregation
Redeeming a note through a plain Cashu swapSwap and melt disabled for BNAP keysets
Paying a cheap challenge for a costly actionScope inside the challenge MAC
Mixed units or overflowOne unit per spend, checked arithmetic
Bloating the store with free swapsPer-keyset input fees
Admission minted by someone elseThe operator accepts only its own keysets
Issuer linking a spend to a purchaseBlinding; DLEQ proofs are never presented
Per-user tagging keysKeysets published 30 days ahead and cross-checked
Wallet fingerprintingPowers of two and mandatory refund outputs
Lost deviceSeed recovery through NUT-13, NUT-09 and NUT-07
Invoice-minting denial of serviceFunding only on BNAP-Fund, capped and rate-limited
OssificationEven/odd TLV rule and GREASE

Accepted residual risks:

  • Linkable first action. A SPEND carrying fund, and every tier-0 Lightning payment, is linkable to its payment.
  • Small operators can still link visitors by IP address, TLS fingerprint and timing; BNAP promises unlinkable notes, not anonymity.
  • Operator credit. An operator that disappears takes unspent credit with it; wallets keep balances small.
  • Store loss. Without BNAP-W, notes spent since the last durable copy can be spent again until keyset expiry, costing free actions only.
  • Key compromise. A stolen issuer key forges notes, so the keyset is revoked and replaced at once; keys live in a separate signer process.
  • Suite 0 is not post-quantum for forgery; blinding keeps past spends private.
  • Unflagged abuse pays only the price; BNAP-B adds deposits where an operator can detect abuse.
  • Regulation. Non-withdrawable prepaid credit for one’s own service reads closer to a merchant voucher than to custody; BNAP-W changes that. Both readings need EU counsel.

Extensions and roadmap

The core is Suite 0 notes, three verbs and the HTTP binding; everything else is a separate, optional document.

DocumentWhat it addsStatus
BNAP coreSuite 0 notes, CHALLENGE, SPEND, refund outputsMandatory
HTTP binding and agent profile402 challenge, content-covering scope, BNAP-FundMandatory for HTTP servers
Buying creditCashu NUT-23, NUT-04 and NUT-20, referenced verbatimMandatory for issuers
L402 and MPP fallbackPer-action Lightning for zero-code agentsRecommended for HTTP
BNAP-WWithdrawalOptional, off by default
BNAP-BDeposits with release or forfeit, forfeit log, claim ratesExperimental
Nostr NIPPriced relay writes with self-issued challengesSecond deployment
SASL+BNAP, ESMTPMail, chat and MQTT carriersOptional
Ark and Cashu-token value-inFunding without LightningExperimental
Suites 1 to 3VOPRF, ACT, KVACReserved
x402 schemeInterop with x402 clientsOptional
Pricing and wallet guidanceLoad-based prices, ceilings, budgetsInformational
TLS/QUIC admissionPricing handshake floodsDeferred
Hashcash, ack-refund, sessions, linkable mode—Removed

Reference implementation: Caparra

Caparra is built on CDK, the Cashu Development Kit, so the cryptography, keysets and mint flows are existing, reviewed code; Caparra adds only the BNAP profile and the agent tooling.

CrateResponsibility
bnapProfile types: SigningBody, Digest, witnesses, challenge TLVs, scope functions, test vectors
caparra-issuerA CDK mint configured as a BNAP issuer: spend store, deterministic replies, separate signer process
caparra-gateReverse proxy and Tower middleware for paid APIs; emits BNAP, L402 and MPP challenges
caparra-walletAgent wallet with seed recovery, budgets and NWC funding; the bnap CLI and MCP server
caparra-lnThe InvoiceSource interface; receive-only NWC by default, LDK Node optional
caparra-w, caparra-nostr, …One crate per extension

Build order: bnap with test vectors, then caparra-issuer, then caparra-gate in front of one real paid API, then the CLI and MCP server, then the NUT pull requests, then the Nostr plugin.

Targets to measure: p95 verification under 2 ms per spend at the default limits, excluding the durable commit, and about 300 bytes of storage per spend.

Standardization and adoption

No part of BNAP is a BIP or a bLIP: nothing touches Bitcoin consensus or the Lightning protocol. It ships as Cashu NUTs first, with running code, and goes to the IETF only after two independent implementations.

OrderDocumentVenue
1NUT-XX “Key-secret proofs with context signatures” (~3 pages): secret = hex(K), witness over a tagged Digest, the new NUT-13 derivation typeCashu NUTs; pitched as making NUT-22 blind-auth tokens theft-proof
2NUT-XX “HTTP 402 with a self-issued mint”, extending NUT-24 and aligned with its X-Cashu header and NUT-18 payment requests: scope, spend, refund outputs, ack, funding in the 402Cashu NUTs
3Agent interop note: BNAP, L402 and MPP in one 402Agent tooling communities
4NIP-XX “Priced relay writes”Nostr NIPs
5Internet-Draft with Suite 1, after two implementationsIETF Privacy Pass or HTTP authentication

Socialize in this order: the Cashu developer chat and call with a CDK branch and cashu-ts test vectors; the NUT pull requests; a Delving Bitcoin post asking for a privacy critique of request-bound blind spends; agent tooling builders; the IETF last.

Minimum lovable deployment. caparra-gate in front of one real paid API, such as a search or inference endpoint, with the bnap CLI and MCP server so agents fund over NWC.

After 90 days, it worked ifTarget
Operators other than the authorAt least one
Funded agents that top up a second timeAt least 30%
Abusive free-tier trafficDown at least 80%, without losing paying users
p95 verification timeUnder 2 ms
Independent implementations passing the test vectorsAt least one, not in Rust

Allies: CDK and Nutshell maintainers, Alby (NWC), OpenSats for grants, and agent-tooling builders. Lightning Labs and the x402 camp are bridge partners, not targets.

Licensing and governance. Code is MIT/Apache-2.0; NUT text follows the Cashu repository. A single maintainer runs public proposals until two independent implementations exist; no maintainer ever holds funds or runs a default issuer.

Appendix A: Rationale (informative)

BNAP is shaped like the protocols that lasted: one small object in the middle, many things below and above it. This appendix explains the shape; it makes no claim that BNAP has earned the comparison, which only deployments can do.

Figure: The BNAP hourglass · informative. The source tab shows this as an interactive diagram (widget node/dd7aab73-de90); 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, with no payment.

  • Narrow waist. IP succeeded because it assumes almost nothing, so anything can carry it and anything can run on it. BNAP’s waist is the Note: value rails below, carriers above.
  • Pay rarely, check cheaply. TLS separates an expensive handshake from a cheap record layer. BNAP separates buying credit on a value rail from spending it with a local check.
  • Protocol and implementation. HTTP is a protocol; Apache is one program that speaks it. BNAP is the protocol; Caparra is one implementation, and others must be able to replace it.
  • End to end. Saltzer, Reed and Clark (1984) argued that functions needing application knowledge belong at the ends. Judging abuse is such a function, so BNAP leaves it to the application.

Appendix B: Debate record

Round 3, 10 October: Bitcoin development state of the art

An ecash cryptographer, an anonymous-credentials expert, a Lightning engineer and a Bitcoin protocol reviewer each reviewed v0.3, then answered each other’s strongest points. Where they still disagreed, the owner decided.

QuestionPositionsVerdict
Note secretRaw K bytes (v0.3) or hex(K) as a Cashu secrethex(K), with a key-isolation rule
Keyset IDCustom 8 bytes or Cashu version-2Version-2, short ID allowed on the wire
RecoveryNone (v0.3) or NUT-13NUT-13 seed recovery; rated critical
Wire formatCustom binary or Cashu CBORCBOR objects; binary only for the signed bytes
Refund after failureack-refund, delete it, or check the databaseClient-supplied refund outputs, signed only on FAILED
Replay storageFull responses or recomputationRecompute; 16-byte tags; re-verify first
First fundingFour steps, fund token in the 402, or a NUT-23 quote in the 402NUT-23 quote, embedded on opt-in
Zero-code agentsBridge extension or a SHOULD in coreSHOULD offer L402 or MPP
Credential typeACT by default, or notesNotes mandatory; ACT a reserved suite
Mandatory railBOLT11 or BOLT12BOLT11; BOLT12 optional
FeesNone or input_fee_ppkPer-keyset input fees
HashcashKeep or dropDropped
WithdrawalExperts: never in v1Owner: optional BNAP-W, off by default
FramingExperts: drop “foundational”Owner: layered spec, modest framing, rationale in Appendix A
First deploymentExperts: a Nostr relayOwner: AI agents and paid APIs; Nostr second

Earlier rounds

  • Round 1, 9 October: the operator issues its own notes, so no global mint allowlist exists; per-action prices beat refundable bonds; application carriers come before transport.
  • Round 2, 10 October: one primitive and three verbs; the spend signature covers the whole message; bonds, the forfeit log and the TLS binding leave the core.
  • Independent reviews of v0.1 and v0.3 found eight blockers between them, all fixed, including the DLEQ linking leak, bearer session ids and replayable retries.

Independent review of the first v0.4 draft found three blockers: the commit order contradicted the FAILED status, a paid fund quote could be redeemed twice, and the signed bytes were not defined to the byte. It also found eight major gaps, including a spend store that Cashu restore could not use, a MAC that missed two fields, uncapped funding, ambiguous L402 single use, restore gaps, a key-rotation contradiction, unbounded replays and HTTP header limits. All are fixed in this version through the PENDING state, the ISSUED quote rule, the SigningBody table and the rules around them.

Open questions

Most open items now need running code or other people, not more design.

  • First API. Pick the real paid API that caparra-gate fronts first.
  • Cashu maintainers. Request NUT numbers and NUT-13 derivation type 0x02; agree how BNAP aligns with NUT-24.
  • Header syntax. Check the L402 and MPP Lightning challenge formats against their current specs before shipping the fallback.
  • EU legal reading of non-withdrawable credit, and of BNAP-W.
  • Suite 1. Confirm the RFC 9380 secp256k1 hash-to-curve suite for the IETF track.
  • ACT. Measure verification at a 24-bit range and watch CFRG adoption before promoting Suite 2.
  • Wallet partners for NWC funding, such as Alby.
  • Name checks: IANA service names, the caparra crate, EU trademarks.
  • Test vectors and benchmarks for every encoding, against the 2 ms target.

Sources

Opened during round 3:

Sources for rounds 1 and 2 are listed in the archive tabs.