Errata: draft-00, reference code and vectors, v0.5 overview
Spec text licensed CC-BY-4.0, (c) BNAP contributors. Compiled 2026-10-13.
This table lists every inconsistency found between three sources:
- draft-00: socket-draft-00.md, doc rev 39. Normative.
- ref / vectors: bnap_ref.py (sha256
aaede669…bbc47d1e) and vectors.json (sha256f21ff83a…4094ae14). - overview: overview-v0.5.md, v0.5.1, doc rev 33.
Rows 30 and 31 are internal to draft-00 or concern the bnap:notes tab; rows 32 to 34 come from findings outside these sources, supplied with the export task; row 35 comes from the end-to-end conformance run against a real gate.
Rule. Draft-00 wins unless a vector proves otherwise. Where draft-00 is silent, the row says so, and adding a normative rule is marked as an owner decision.
Draft-00 against the vectors. No row: they agree. All 11 full hex values and all 9 shortened values in draft-00 section 14, and both hex blocks (the 305-byte offer and the Response), match vectors.json. The base64url prefixes in the 10.3 example decode to the start of the vector offer, credential and Response, and BNAP-Error: AAEBAgEQ decodes to code 16. The 10.3 arithmetic holds (1,680 sat ceiling, 425 sat before and 340 sat after the discount). The BIP-340 code reproduces official vector 0, and regenerating the vectors gives identical bytes.
| # | where | draft-00 says | other source says | resolution (draft-00 wins unless the vector proves otherwise) | owner decision needed (yes/no) |
|---|---|---|---|---|---|
| 1 | Credential encoding: draft-00 2, 5.1, 15; overview “Credential envelope” | The Credential is a TLV stream: 0 version, 2 offer, 4 pay, 6 attest, 8 binding, 10 ext, 12 offer_id; exactly one of offer and offer_id | Overview: Credential = {v, offer, pay?, attest[], binding, ext} in deterministic CBOR, with no offer_id. Ref: the credential vector (581 bytes) is TLV | TLV, as draft-00 section 15 lists; the vector agrees. The overview is to be brought into line | no |
| 2 | Scheme payloads: draft-00 5.1; overview “Credential envelope” | Each scheme sends a body and a witness; only class S has a witness; the Digest covers bodies, never witnesses | Overview: pay = {scheme, payload}, attest = {scheme, payload}, and the Digest hashes each payload. Ref: the notes body and witness are separate records (types 2 and 4 of the pay stream), and the witness signs the Digest | Body and witness, as draft-00 section 15 lists (the v0.5.1 Digest was circular) | no |
| 3 | Context: draft-00 5.2; overview “Credential envelope” | H_"bnap/ctx"(OfferID ‖ action_scope ‖ lp(pay scheme ID, or empty) ‖ BigSize(n) ‖ lp(attest scheme ID₁) ‖ … ‖ H(ext value, or empty)) | Overview: H_"bnap/ctx"(OfferID ‖ value scheme ID ‖ sorted attestation scheme IDs ‖ H(ext)), with no action scope, length prefixes or count. Ref: context() follows draft-00; credential.context = e9f56036…12158e6d | Draft-00; the vector proves it | no |
| 4 | Digest: draft-00 5.2; overview “Credential envelope” | H_"bnap/cred"(Context ‖ lp(binding, or empty) ‖ H(pay body, or empty) ‖ H(attest body₁) ‖ …) | Overview: H_"bnap/cred"(Context ‖ binding ‖ H(pay payload) ‖ H(each attest payload)), binding without a length prefix. Ref: digest() follows draft-00; credential.digest = 7c8f6196…d530d4c9, digest_other_binding = c9ef748b…a36a207a | Draft-00; the vector proves it | no |
| 5 | ReplayKey: draft-00 6.1; overview “Credential envelope” | H_"bnap/replay"(Context ‖ H(pay body, or empty) ‖ H(attest body₁) ‖ … ‖ BigSize(m) ‖ id₁ ‖ … ‖ idₘ) over every UseId and RailId, sorted bytewise | Overview: H_"bnap/replay"(Context ‖ sorted socket use IDs). Ref: replay_key() follows draft-00; credential.replay_key = 74bd8a79…9b0c2771, attest_only.replay_key = c9248cf0…8961be9c | Draft-00, as section 15 lists; the vector proves it | no |
| 6 | Response and ack: draft-00 8.1, 15; overview “Receipt” | The Response is a TLV stream of 11 records (version, status, replayed, charged, unit, offer_id, digest, metered_units, scheme_result, receipt_key, settled_at) plus ack (type 240) over H_"bnap/ack"(every record outside 240–1000). Section 15 calls this “Same fields as v0.5.1’s formula” | Overview: Response = {status, replayed, scheme_result, ack} with the ack over H_"bnap/ack"(Digest ‖ status ‖ charged ‖ unit ‖ H(scheme_result)). Ref: build_response() signs the whole stream; the vector Response holds types 0, 2, 6, 8, 10, 12, 14, 16, 18, 20 and 240, ack c1d4270a…bfef6f70f7 | Draft-00; the vector proves it. The stream signs more than the v0.5.1 fields (also version, replayed, offer_id, metered_units, receipt_key, settled_at), so section 15 should say “a superset of v0.5.1’s fields” | no |
| 7 | Signature range and coverage: draft-00 2, 4.2; overview “Offer”, “Extensibility” | Signature records are types 240–1000. OfferID = H_"bnap/offer"(S₀) over every record except the signature range and native; offer_sig = Sign(offer_key, H_"bnap/offer-sig"(S)) and mac = HMAC-SHA256(K_mac, S), where S is every record outside the signature range | Overview: “Types 240 to 255 are signature records”; OfferID over “every record below 240 except native”; offer_sig and mac over “every record below 240”. Ref: sig_range() is 240–1000; the vector offer’s GREASE record (type 3,000,000,001, after mac in stream order) is inside OfferID, offer_sig and mac | Draft-00, as section 15 lists; the vector proves that records above 1000 are covered | no |
| 8 | Offer record 14: draft-00 4.1; overview “Offer” | offer_key: the 32-byte BIP-340 key that signed the offer | Overview: op_key_id, “Which online offer key, listed in the manifest, signed this offer”. Ref: record 14 of every vector offer holds the key itself, b28dad5f…fe64add9 | Draft-00; the vector proves it | no |
| 9 | Offer record set: draft-00 4.1, 4.3, 4.4; overview “Offer” | Adds route_scope (4), headers (6), the pay-entry rate (10) and the attest-entry value and epoch. mac may be absent only in offers published in advance, which then MUST carry issued_at. native only in exact-scope offers | Overview: no types 4 or 6; pay entries have no rate; attest entries list only scheme ID, parameters and effect; issued_at is for “Self-issued offers only”; scope is always “32-byte hash of the action”. Ref: the route offer vector uses records 4 and 8 and a pay entry with rate 500 | Draft-00 (section 15 lists route scope and rate); the vector agrees | no |
| 10 | Metering: draft-00 7, 8.1; overview “Uses and outcomes” | rate is the price per 1,000 meter units; charged = min(price(⌈units × rate / 1000⌉), price(amount)); the Response carries metered_units and charged | Overview: “For metered actions, the charge is the amount used”; no rate, no metered_units. Vectors: Response charged 340, metered_units 850 | Draft-00, as section 15 lists; the vector agrees | no |
| 11 | Discounts and route member names: draft-00 4.1, 9.1; overview “Scheme properties and route policy” | Attest value is in basis points, 1 to 10,000. Manifest routes use pay and attest: [{"scheme", "effect", "value", "epoch"}] | Overview TOML: accept = [...] and attest = [{ scheme = "httpsig:web-bot-auth", effect = "discount", percent = 20 }]. Vectors: manifest "pay": [...] and "value": 2000; offer attest entry value 2000 | Draft-00; the vector proves basis points and pay. The overview example and the Caparra config should follow | no |
| 12 | Effect in the manifest: draft-00 9.1 | Routes list effect but the text does not say how it is written in JSON (the Offer uses tu64 0 to 3) | Vectors: the manifest writes "effect": "discount"; the overview TOML also uses effect = "discount" | Draft-00 is silent; the vector settles it: string names required, waive, discount, quota. State this in 9.1 | no |
| 13 | Metered route in the manifest: draft-00 9.1, 10.3 | Routes have price: {unit: amount}, metered and meter, but no rate member, and the meaning of price on a metered route is not stated. 10.3 describes the route as 500 sat per 1,000 tokens up to 2,100 sat per call | Vectors: the manifest has "price": {"sat": 2100}, the ceiling; the rate 500 appears only in the offer | The vector shows that price is the ceiling. Whether the manifest should also carry the rate is open | yes |
| 14 | Error codes: draft-00 8.2, 15; overview “Errors” | 16 numbered codes. Section 15 names UNDERPAID, BAD_CREDENTIAL, BINDING and UNAVAILABLE as new, but not REQUIRED (16) | Overview: 11 codes, unnumbered, no REQUIRED. The 10.3 example’s BNAP-Error: AAEBAgEQ decodes to 00 01 01 02 01 10, code 16 | Draft-00’s table. Add REQUIRED to the section 15 row | no |
| 15 | HTTP header fields: draft-00 10.2; overview “Negotiation”, “Carriers” | Offer in WWW-Authenticate: BNAP offer="…"; credential in BNAP-Credential; receipt in BNAP-Receipt (or a trailer, or a final bnap-receipt event); errors in BNAP-Error; optional BNAP-Context. The manifest URI is only Offer record 12 | Overview: credential in Authorization: BNAP …, receipt in Authentication-Info: bnap="…"; the carriers table lists WWW-Authenticate, Authorization, Authentication-Info; the 402 example adds info="/.well-known/bnap" as an auth-param. Vectors: the offer’s info record is the absolute URI https://api.example/.well-known/bnap | Draft-00, as section 15 lists | no |
| 16 | HTTP scope: draft-00 10.1; overview “Carriers” | lp(method) ‖ lp(target) ‖ lp(content hash, or empty) ‖ BigSize(h) ‖ listed headers; target is the manifest origin plus the request-target exactly as received, with no normalization; the gate hashes the body itself and Content-Digest is only a SHOULD | Overview: the scope covers “Method, normalized absolute URI, Content-Digest”. Ref: http_scope() follows draft-00; action scope 02be38b5…971b3717 | Draft-00, as section 15 lists; the vector proves it | no |
| 17 | Native envelope: draft-00 5.4, 15; overview “Negotiation” | Context = H_"bnap/ctx-native"(OfferID ‖ action_scope ‖ lp(scheme ID)), Digest = H_"bnap/cred-native"(Context ‖ H(native credential bytes)); offers cached by OfferID; receipt in the BNAP response header. Section 15 does not list this change | Overview: Context = H_"bnap/ctx"(OfferID ‖ scheme ID), Digest = H_"bnap/cred"(Context ‖ H(native credential bytes)), the same tags as a BNAP Credential; cache keyed by nonce; receipt in Authentication-Info. Ref: native_context() and native_digest() follow draft-00; context 38a036b6…6cbb1b78, digest 198cd8d6…f0a2a04e | Draft-00; the vector proves it. Add a section 15 row | no |
| 18 | UseId and RailId form: draft-00 6.1; overview “Uses and outcomes” | UseId = H_"bnap/use"(lp(scheme ID) ‖ local ID) and RailId = H_"bnap/rail"(lp(rail kind) ‖ canonical payment bytes), 32-byte hashes; evm bytes are lp(CAIP-2 chain ID) ‖ token ‖ payer ‖ authorization nonce | Overview: string prefixes such as bnap:notes:<Y>, httpauth:L402:<payment hash>, ln:<payment hash>, evm:<chain>:<token>:<from>:<nonce>. Ref: use_id() and rail_id() follow draft-00; use IDs b7ca97d6…232b6456, 381a38d9…4bc2fb5e; rail ID 5f210bd3…f1867744 | Draft-00; the vector proves it. Section 15 lists only who supplies the rail bytes; consider a row for the hashing too | no |
| 19 | Who supplies rail IDs; plug traits: draft-00 6.1, 6.3; overview “Uses and outcomes”, “Reference implementation” | The scheme returns a rail reference; the operator-trusted rail plug confirms it and returns the canonical bytes; the gate computes the RailId. commit(rk, singles, counters) also takes rate and quota counters | Overview: “A scheme that relies on a rail payment MUST also report that payment’s canonical rail ID”; the Rust sketch has Verified { rail_ids: Vec<RailId> }, UseStore::commit(ids, key) with no counters, and no body and witness split | Draft-00, as section 15 lists. The Caparra trait sketch should follow | no |
| 20 | Use models: draft-00 6.2, 7; overview “Uses and outcomes” | single, rate(k, epoch) and reusable, where reusable consumes nothing beyond the outcome record; limits come from a quota attestation (effect 3) counted against a registered quota_id | Overview: reusable(quota), “A metered allowance per use ID” | Draft-00 | no |
| 21 | Timeout defaults: draft-00 2, 9.1; overview “Uses and outcomes” | limits holds replay_window and pending_timeout, with no default for either and no rule that they are present. Only the size limits have defaults | Overview: “pending_timeout (default 300 s)”; Caparra config replay_window_s = 86400, pending_timeout_s = 300. Vectors: the manifest uses 86400 and 300 | Draft-00 is silent; the vector is an example, not a default. Choose defaults (300 s and 86,400 s) or make both members required | yes |
| 22 | ES256 co-signature: draft-00 8.1, 15; overview “Receipt” | One ack, BIP-340, type 240; no other signature record is defined. Section 15: the 240–1000 range leaves “Room for more signature types later” | Overview: “An operator whose keys live in an HSM MAY add an ES256 signature as well; BIP-340 stays mandatory” | No ES256 record exists in draft-00. Keep the MAY only if a record type is assigned | yes |
| 23 | GREASE scope: draft-00 2; overview “Offer”, “Extensibility”, “Negotiation” | GREASE is TLV types 3,000,000,001 to 3,000,000,999, odd only; senders SHOULD add one record to every Offer and Credential | Overview: “scheme IDs and carrier names reserve GREASE values too”; the 402 example lists GREASE inside pay; servers SHOULD include one. Vectors: the offer has one GREASE record (value 0a0a); the credential has none, which a SHOULD allows | Draft-00 defines no GREASE scheme IDs or carrier names. Keep or drop that overview feature | yes |
| 24 | Native challenge minting: draft-00 12; overview “Negotiation” | The gate MUST bound native challenges per client address and per offer; it SHOULD mint native challenges at most once per offer nonce | Overview: “A server MUST mint at most one invoice per offer nonce” | Draft-00 wins by rule (SHOULD), but this weakens a MUST and section 15 gives no reason | yes |
| 25 | Class None value schemes: draft-00 4.3, 7, 12; overview “Credential envelope” | Class None needs an exact-scope offer (4.3), never appears in a route-scope offer (12), and may not waive, discount or carry a quota (7). Nothing limits a None value scheme to price 0 | Overview class table: None is “Allowed only at price 0, and never with a waive or discount effect” | Draft-00 is silent on the price-0 half, so the overview rule is not contradicted. Adding it to draft-00 section 7 is a normative change | yes |
| 26 | Unit strings: draft-00 4.1; overview “Offer”, “Value rails” | sat, msat, an ISO 4217 code (minor units) or a CAIP-19 asset ID | Overview: “sat, an ISO 4217 code or a CAIP-19 asset”, no msat; the guardrails write usd, eur in lower case. Vectors: sat only | Draft-00: msat is allowed, and fiat units are ISO 4217 codes, which are upper case (USD, EUR). Draft-00 could say that units compare exactly | no |
| 27 | Manifest and pinning: draft-00 9.1 to 9.3; overview “Manifest and pinning” | Member required (the bnap-required flag); bnap: 1, origin, issued. A manifest signed by the current root keeps the same backup; the backup signs only a rotation; accept a higher seq or identical bytes; issued checked with 60 s of skew; pin until min(issued + max_age, now + 30 days); no two rotations within 30 days | Overview: “A manifest MAY set bnap-required”; it “lists versions”; a client accepts a refetched manifest “if the root or the backup signed it with a higher seq”. Vectors: the manifest uses required, bnap, origin, issued and backup | Draft-00, as section 15 lists for the backup and issued-time rules | no |
| 28 | Tab references: draft-00 1, 12, 15; overview “Abstract and status”, “Security and privacy considerations” | Calls the overview “the first tab”, which is still true | Overview: the bnap:notes spec is “the second tab” and draft-00 is the “third tab”; the doc now orders the tabs overview, draft-00, bnap:notes, v0.3, v0.2 | Texts stay verbatim; README.md maps each tab reference to its file | no |
| 29 | Stale open question: draft-00 4.1, 14; overview “Open questions” | Defines the pay and attest encodings and publishes vectors | Overview, unticked: “Socket byte layout. Define the Offer’s pay and attest record encodings and publish test vectors” | Done by draft-00 and vectors.json; tick it when the overview is brought into line | no |
| 30 | Header size, internal to draft-00: draft-00 2, 10.2 | A Credential may be up to 16,384 bytes (2); gates MUST accept a BNAP-Credential field of at least 12,288 bytes (10.2) | Arithmetic: base64url of 16,384 bytes is 21,846 characters, and 12,288 characters carry 9,216 bytes. Vectors: the credential is 581 bytes (775 characters), far below both | No vector settles it. Raise the field minimum to 21,846 bytes, or cap Credentials on HTTP at 9,216 bytes | yes |
| 31 | bnap:notes tab, outside the three-way comparison: draft-00 2, 5.1, 11; schemes/bnap-notes.md | Schemes split body and witness. The bnap:notes registry example gives body = "TLV, section 3 of the scheme spec" and a witness over the socket Digest; the credential travels in BNAP-Credential. Section 2 lets a scheme keep JSON or CBOR payloads | The bnap:notes tab is still the v0.4 profile: a CBOR SPEND, its own H_"bnap/spend" Digest and ack, Authorization: BNAP (or BNAP-Authorization) and Authentication-Info. It has no numbered section 3 | Draft-00 wins for every socket part. The notes scheme spec needs a draft-00 rewrite, and its body encoding (TLV, as the registry example says, or CBOR) needs choosing | yes |
| 32 | x402 on Lightning (external finding): overview “Abstract and status”, “Plug catalogue”, “Badges”, “Roadmap”, “Open questions” | Nothing beyond the RailId rule: one Lightning payment offered under L402, MPP and x402 admits one action | Overview: owner decision “No x402 on Lightning until its spec is published”; catalogue “Pending a published spec”; Agent-1 and v0.6 “once its spec is published”; sources link coinbase/x402. External: the x402 exact scheme on Lightning (lnbtc) was published 2026-09-23 in x402-foundation/x402 | The Agent-1 condition is met; x402 on Lightning stays deferred to v0.6. Update the catalogue and badge wording. The open question on the network label and on a client-chosen nonce (class O or A) can now be checked against the published spec | no |
| 33 | MPP receipts (external finding): draft-00 5.4, 10.2; overview “Negotiation” | The receipt “goes in the BNAP response header next to the scheme’s own”, that is BNAP-Receipt | Overview: “the receipt goes in Authentication-Info next to the scheme’s own”. External: MPP (draft-httpauth-payment-01) returns its receipt in a Payment-Receipt header, not Authentication-Info | Draft-00: BNAP-Receipt travels next to MPP’s Payment-Receipt. The MPP registry entry should name that field | no |
| 34 | L402 (external finding): draft-00 4.1, 5.4, 10.3; overview “Plug catalogue”; bnap:notes “Sources” | Native credential bytes “such as the L402 macaroon:preimage token”; the 10.3 example sends WWW-Authenticate: L402 macaroon="…", invoice="…" | Overview catalogue: “L402 (bLIP-26)”; the bnap:notes tab links bLIP-26 at blob/master/blip-0026.md. External: bLIP-26 is still an unmerged PR | Cite bLIP-26 as an open PR. The L402 adapter accepts both token= and macaroon=, and both L402 and LSAT. Draft-00’s example stays as it is | no |
| 35 | MPP native entry (end-to-end finding): draft-00 4.1, 4.2 step 3, 5.4; registry payment:lightning/charge | The native artifact embeds the OfferID “in the way its scheme’s registry entry defines, such as … an MPP id”; 5.4 lists “an HMAC’d MPP id” as a stateless artifact | Registry (before): mpp-challenge “the challenge’s HMAC’d id carries the OfferID”, and a bolt11 entry listed beside it as if always present. draft-httpauth-payment-01: the id is the server’s HMAC over the bound challenge parameters, so it cannot also be the OfferID; opaque is the parameter for server data. Caparra’s gate and the conformance runner (profile bnap-conf-mpp-2): the mpp-challenge entry is the Payment challenge exactly as sent, the OfferID is in opaque as base64url of the JCS object {"bnap_offer": hex(OfferID)}, covered by the HMAC id, and the invoice is methodDetails.invoice inside the challenge | The OfferID goes in opaque, under the HMAC’d id; a gate that puts it in id itself fails OFR-NATIVE-EMBED. A bolt11 entry is optional for MPP. The registry wording is updated; draft-00 4.2 should read “an MPP opaque covered by its HMAC’d id” in the next revision | no |
35 rows; 8 need an owner decision (13, 21, 22, 23, 24, 25, 30, 31).