PROTOCOL_FLOWS.md raw

Musiquay — Protocol Message Flows

ARCHITECTURE.md defines the message types — the kind registry. This document defines the message flows — who sends what, to whom, in what order, over which transport, and what each side must verify before acting.

1. Scope and Status

This is a wire-level companion to the kind registry. It is implementation-neutral: it describes WebSocket frames, HTTP requests, and signed events, not APIs or code structure. Anything that cannot be observed on the wire does not belong here.

It covers, in order:

Status markers are used on every flow:

MarkerMeaning
[SPEC]Already defined in the existing docs. This document only makes the sequence explicit.
[NEW]Defined here for the first time. Normative until superseded.
[OPEN]Cannot be finalised until a decision in §12 is made.
[IMPL]Gap between the current prototype (orly + gonuts) and the spec.

This document does not define client UI, payout policy, moderation, or the custodial identity chapter. It also does not redefine the roles and validation philosophy — see VISION.md and FEATURES_ROSTER.md.

2. Actors

A Nostr keypair is the only identity primitive. Everything below is a keypair that may be held by a person, a company, a service, or a machine.

ActorKeyRole in flows
Publisher / OwnerK_ownAuthor of the track/album event. Sets price, authors access filters, receives the ownership fee, archives receipts, publishes rollups.
ArtistK_artTagged participant (p tag), not necessarily the author. Confirms credits.
HostK_hostBlossom server that stores and serves blobs. Verifies filters, gates access, issues receipts, signs plays, publishes aggregates.
RetailerK_retOperates a channel: storefront, curation, delivery rails. Authors its own per-asset and subscription filters. Sells subscriptions, never a cut of the owner's fee.
Buyer / ListenerK_buyPays, joins filters, downloads, streams, publishes play reports.
Relay—Stores and serves events (NIP-01). May require AUTH (NIP-42).
MintK_mintCashu mint: issues and redeems ecash (NUT-00..26).
Lightning node / LNURLK_lnBOLT-11 settlement; NIP-57 zaps for the Lightning payment leg.
Sales serviceK_own or delegatedWatches for a settled purchase order, validates it, rebuilds the filter. May be the owner's client, a retailer, or an external service acting for the owner.
ClientK_usrAny app. Holds the user key, talks to relays (WS), hosts (HTTP), mints (HTTP).

Roles compose. The owner is often the host and the publisher. A retailer is a host that also sells subscriptions. Nothing in the protocol privileges one role over another.

3. Transports and Message Primitives

3.1 Nostr relay (NIP-01, NIP-42, NIP-45)

Client → relay:

MessageFormUse
EVENT["EVENT", <event>]Publish any signed event.
REQ["REQ", <sub_id>, <filter>, ...]Subscribe to stored + live events.
CLOSE["CLOSE", <sub_id>]End a subscription.
COUNT["COUNT", <query_id>, <filter>, ...]Count matching events (NIP-45).
AUTH["AUTH", <kind 22242 event>]Answer a relay challenge (NIP-42).

Relay → client:

MessageFormUse
EVENT["EVENT", <sub_id>, <event>]Stored or live event.
EOSE["EOSE", <sub_id>]End of stored events; live tail follows.
OK["OK", <event_id>, <true\|false>, "<prefix>: <msg>"]Publish result; the only publish acknowledgement.
CLOSED["CLOSED", <sub_id>, "<prefix>: <msg>"]Subscription refused or killed; also used when COUNT is refused.
NOTICE["NOTICE", "<msg>"]Human-readable diagnostic.
COUNT["COUNT", <sub_id>, {"count": <n>}]Count result (may include "approximate": true and/or "hll").
AUTH["AUTH", "<challenge>"]Relay requires authentication.

Rules that the flows depend on:

event id. References to tracks, albums, filters, credits, and calendar events MUST use a tags. References to receipts, aggregates, rollups, and play reports use e tags.

does not. This constrains asset/blob lookup — see §12 G-1.

ties on created_at are broken by lowest event id.

3.2 Blossom (BUD-01 … BUD-12)

Blossom is plain HTTP. All endpoints are served from the domain root, all pubkeys are hex, and every error response MAY carry X-Reason (human-readable only — never parsed for control flow).

EndpointBUDPurpose
GET /<sha256>[.ext]01Fetch a blob. Supports Range (206).
HEAD /<sha256>[.ext]01Existence, Content-Type, Content-Length, Accept-Ranges.
PUT /upload02Upload. 201 new, 200 already present, body is a blob descriptor.
HEAD /upload06Pre-flight with X-SHA-256, X-Content-Length, X-Content-Type.
PUT /mirror04Ask a server to mirror a blob from a URL.
PUT /media, HEAD /media05Media optimization (variants).
DELETE /<sha256>12Delete a blob.
GET /list/<pubkey>12List a pubkey's blobs (unrecommended).
PUT /report09Report a blob.

Authorization (BUD-11): a signed kind 24242 event, base64url-encoded without padding, presented as

Authorization: Nostr <base64url(kind-24242 event)>

The token MUST have content explaining the use, a NIP-40 expiration tag in the future, a t tag with verb get | upload | list | delete | media, and MAY carry server (domain only) and x (blob hashes) scoping tags.

This is how a host learns a requester's pubkey. There is no NIP-42 in HTTP. Earlier drafts of FEATURES_ACCESS.md said "401 + NIP-42 auth prompt" for a Blossom download; that has been corrected to **401 + BUD-11 Authorization: Nostr challenge** (§12 G-2).

Payment (BUD-07):

HTTP/1.1 402 Payment Required
X-Cashu: <NUT-24 payment request, creqA or creqB>
X-Lightning: <BOLT-11 invoice>          # optional alternative

retries the GET/PUT.

X-Lightning: <preimage>) and no other change.

3.3 Cashu (NUTs)

Mint endpoints used by these flows (paths as implemented by gonuts):

Method + pathNUTPurpose
GET /v1/info06Mint info, supported NUTs and units.
GET /v1/keys, GET /v1/keysets01/02Keysets and mint public keys.
POST /v1/mint/quote/bolt1104Request a mint quote (BOLT-11).
GET /v1/mint/quote/bolt11/<quote_id>04Poll quote state.
POST /v1/mint/bolt1104Redeem a paid quote for blinded signatures.
POST /v1/swap03Redeem/burn proofs, receive new ones. This is also how a seller claims a payment.
POST /v1/checkstate07Proof state (UNSPENT/SPENT/PENDING).
GET /v1/ws17WebSocket notifications: bolt11_mint_quote, proof_state.

Relevant primitives:

carry a memo m. New flows use V4.

B_ = Y + rG; mint returns C_ = kB_; wallet unblinds C = C_ − rK = kY.

signing key. See §8.2 — the current play-report design omits this and is therefore unverifiable (§12 G-3).

identity-bound tickets.

3.4 Private delivery (NIP-59 + NIP-44)

Three layers:

LayerKindSigned byVisible
rumorany kind, unsigned—nothing (deniable)
seal13real authorauthor pubkey only
gift wrap1059 (stored) / 21059 (ephemeral)random one-time keyrecipient p tag only

Each wrap carries exactly one ["p", <recipient>]. A rumor addressed to two parties is wrapped twice. kind 1059 is for asynchronous, persistent delivery; kind 21059 is for live-only payloads. Timestamps of seal and wrap SHOULD be independently randomized into the past. Relays SHOULD only serve 1059 events to the p-tagged recipient and SHOULD gate them behind AUTH (NIP-42).

3.5 Addressing conventions

ObjectAddress or idNotes
Tracka: 32210:<publisher>:<d>d SHOULD be the ISRC when one exists.
Albuma: 32211:<publisher>:<d>d SHOULD be the MusicBrainz release-group MBID.
Playlista: 32212:<author>:<d>NIP-51 set conventions.
Setlista: 32213:<author>:<d>References the roster address.
Release/track creditsa: 32214:…, a: 32215:…Track credits reference both a track and the release credits.
Editiona: 32216:<publisher>:<d>References the album address.
Access filtera: 32217:<owner>:<d> where d is <tier>:<asset-address>tier ∈ fan | retail | sub.
Play reporte: <id>Regular; d-less; counted with NIP-45.
Delivery receipte: <id>Never public; exists only inside wraps.
Host aggregatee: <id>Regular, public.
Publisher rollupe: <id>Regular, public; references aggregates by e.

Filter d values, exactly:

fan:<asset-address>                     e.g. fan:32210:<owner>:<track-d>
retail:<asset-address>                  authored by the retailer
sub:<retailer-pubkey>:<channel-d>       authored by the retailer

An asset address is always a full <kind>:<pubkey>:<d> string, so a d value contains colons. That is intentional and matches NIP-01 (d is opaque).

4. Flow Index

#FlowActorsTransportStatus
P1Upload a blobclient → hostBlossom[SPEC]
P2Publish a trackpublisher → relayNostr[SPEC]
P3Add an encodingpublisher → host, relayBlossom + Nostr[SPEC]
P4Publish an albumpublisher → relayNostr[SPEC]
P5Publish a playlistcurator → relayNostr[SPEC]
P6Catalog bootstrapclient → relayNostr[NEW]
A1Publish price and fan filterowner → relay, hostBlossom + Nostr[SPEC]
A2Authorize a retailerowner → relayNostr[SPEC]
A3Retailer filtersretailer → relayBlossom + Nostr[SPEC]
A4Direct ownership purchasebuyer ↔ ownerNostr + Cashu/LN[NEW]
A5Retailer ownership purchasebuyer ↔ retailer ↔ ownerNostr + Cashu/LN[NEW]
A6Subscriptionbuyer ↔ retailerNostr + Cashu/LN[NEW]
A7Lapse and rebuildretailer → relayNostr[SPEC]
A8Revocationowner/retailer → relayNostr[NEW]
A9Entitled re-downloadbuyer → hostBlossom[SPEC]
A10Host gating decisionhost → relayNostr + Blossom[SPEC]
A11Host onboarding (federation)host → relay, ownerNostr + Blossom[SPEC]
A12Superseded filter cleanuphost → hostBlossom[SPEC]
A13Rental stream bucketlistener ↔ hostBlossom + Cashu[SPEC]
A14Bytes feeclient ↔ hostBlossom + Cashu[SPEC]
D1Issue a delivery receipthost → buyer, publisherNIP-59[SPEC]
D2Publish a host aggregatehost → relayNostr[SPEC]
D3Publish a publisher rolluppublisher → relayNostr[SPEC]
R1Free play reportlistener → relayNostr[SPEC]
R2Paid play report (BDHKE)listener ↔ host, relayBlossom + Nostr[OPEN]
R3Now-playing statuslistener → relayNostr[SPEC]
S1Calendar event + RSVPcreator, attendee → relayNostr[SPEC]
S2Waitlist join and promotionattendee, creator → relayNostr (+ DM)[OPEN]
S3Ticket pre-salebuyer ↔ seller → relayNostr + LN[OPEN]
S4Publish the rostercreator → relayNostr[SPEC]
S5Confirm, deny, correctparticipant → relayNostr[SPEC]
S6Attestwitness → relayNostr[SPEC]
S7Wiki contributioncontributor → relayNostr[SPEC]
S8Setlistauthor → relayNostr[SPEC]
S9Credits and editionsartist/label → relayNostr[SPEC]
S10Merch storefront and ordermerchant ↔ customerNostr (+ LN)[SPEC]
S11Resale and services listinglister ↔ respondentNostr[SPEC]
O1Relay auth for gated writesclient ↔ relayNostr[SPEC]
O2Replication and mirroringrelay ↔ relay, host ↔ hostNostr + Blossom[SPEC]
O3Deletion and vanishauthor → relay, hostNostr + Blossom[SPEC]
O4Recovery and restoreclient → mint, relayCashu + Nostr[NEW]

5. Publishing Flows

P1 — Upload a blob to a host [SPEC]

Precondition: the client holds the bytes and a keypair K_usr.

  1. Optional pre-flight:

HEAD /upload with X-SHA-256, X-Content-Length, X-Content-Type and Authorization: Nostr <24242 t=upload, x=<sha256>>. 200 ⇒ proceed; 402 ⇒ pay the X-Cashu challenge and proceed; 4xx ⇒ stop.

  1. PUT /upload with the binary body, Content-Type, Content-Length,

X-SHA-256, and the same Authorization header.

  1. Host replies 201 Created (new) or 200 OK (already present) with a blob descriptor

{url, sha256, size, type, uploaded}.

Invariants: the server MUST NOT modify bytes; the sha256 is over the exact bytes sent. The client MUST verify the returned sha256 matches its own computation.

P2 — Publish a track [SPEC]

  1. For each encoding: P1. Collect (sha256, codec-spec, server-url, media-type, size).
  2. Build the track event (kind 32210, see FEATURES_MUSIC.md) with

d, metadata tags, one audio tag per encoding, exactly one master tag, and (for paid assets) a price tag.

  1. ["EVENT", <track>] to each write relay.
  2. Relay replies ["OK", <id>, true, ""] (or a machine-readable failure prefix:

invalid:, blocked:, rate-limited:, restricted:, duplicate:, error:).

The d tag is the recording identity. The event id changes on every replacement; all references use the address.

[NEW] To make blob → asset resolution a standard relay query (§6 A10), the track event SHOULD carry one single-letter ["x", "<sha256>"] tag per encoding, in addition to each audio tag. See §12 G-1.

P3 — Add an encoding [SPEC]

  1. P1 for the new blob.
  2. Re-publish the complete track event: all previous audio tags plus the new one,

same d, fresh created_at.

  1. Relays replace the previous version at the same address. Subscribers to the address see

the new event; clients that cached the old event id MUST NOT keep referencing it.

P4 — Publish an album [SPEC]

  1. Each track already published (P2), each with a stable address.
  2. Publish 32211 with d, metadata, price if the release as a whole is priced, and the

ordered a tags. Tag order is the tracklist.

  1. Relays store it addressably. Editions (32216) and credits (32214) reference the album

address afterwards.

P5 — Publish a playlist [SPEC]

  1. Build 32212 with d, title, description, image, and ordered a tags.
  2. Public: tags are plaintext. Private: the member list is encrypted into .content

(NIP-44, self-key), with no member a tags.

  1. ["EVENT", <playlist>].

P6 — Catalog bootstrap [NEW]

A cold client with a user follow list resolves a usable catalog with a bounded set of subscriptions. This sequence is a convention, not a requirement, but implementations SHOULD match it so relays can cache predictably.

# 1. profiles and server lists
["REQ","boot-profiles",{"kinds":[0,10063],"authors":[<followed...>]}]
# 2. recent releases by followed publishers
["REQ","boot-releases",{"kinds":[32211,32210],"authors":[<followed...>],"limit":200}]
# 3. access filters for anything priced (also used for the free/paid decision)
["REQ","boot-filters",{"kinds":[32217],"authors":[<followed...>],"limit":500}]
# 4. credits and editions for the visible albums (by address, second pass)
["REQ","boot-credits",{"kinds":[32214,32215,32216],"#a":[<album-addresses...>]}]
# 5. play totals, if the client wants counts rather than raw reports
["REQ","boot-aggregates",{"kinds":[32219,32220],"#a":[<asset-addresses...>]}]

Then, per visible album, a second pass on its track addresses:

["REQ","boot-tracks",{"kinds":[32210],"#d":[<track-d-tags...>]}]

Notes:

addresses without needing an a filter. When an #a filter is available it is preferable, because it binds kind, author, and d in one value.

what it needs; a catalog subscription left open is a live feed.

reuse the bootstrap ids.

6. Paid Access Flows

A1 — Publish the price and the fan filter [SPEC]

The owner publishes the price on the asset, and a filter event per tier.

  1. Asset event carries ["price","<sats>","sat"]. Absent ⇒ free.
  2. Optional [NEW]: the filter event MAY carry one or more ["mint","<url>"] tags listing

accepted Cashu mints, and MAY carry ["mint","lightning"] if it accepts Lightning directly (§12 G-6).

  1. Build the Bloom element set — the pubkeys entitled to the asset — and serialise the

filter blob:

` magic "MQF1" | version | asset address | fp | n | k | bit array `

Parameters: fp ≤ 1e-6 (spec minimum 1e-9), k = round(−log2(fp)), bit count m = ceil(n·k / ln 2).

  1. PUT /upload the blob (P1). Record the returned sha256.
  2. Publish kind 32217:
{
  "kind": 32217,
  "pubkey": "<owner>",
  "content": "",
  "tags": [
    ["d", "fan:32210:<owner>:<track-d>"],
    ["filter", "bloom"],
    ["fp", "1e-9"],
    ["n", "1234"],
    ["k", "30"],
    ["x", "<filter-blob-sha256>"],
    ["server", "https://blossom.musiquay.com"],
    ["retailer", "<retailer-pubkey>"],
    ["expiration", "1799999999"]
  ]
}
  1. Relays store it addressably. Hosts that serve the asset subscribe to the address and

replace their cached copy on every new version:

` ["REQ","filter-fan-<asset-d>",{"kinds":[32217],"authors":["<owner>"],"#d":["fan:<asset-address>"]}] `

Append vs rebuild. Bloom insertion only sets bits, so a sale can publish old bits ∪ bits(new pubkey) with n+1, without the full element list. Removal cannot be done this way: revocation requires the owner to rebuild from the retained element set. Owners MUST retain the element set locally; the published filter is a derived artifact.

A2 — Authorize a retailer [SPEC]

The owner adds ["retailer","<retailer-pubkey>"] to the fan filter event and republishes it. That single tag is the whole authorization. The owner can go offline afterwards: the host checks the retailer list from the owner-signed filter, and each retailer signs its own filters.

A3 — Retailer filters [SPEC]

The retailer authors, for each asset it sells:

ownership through it;

subscribers, optionally with ["catalog", "<asset-address>", ...] coverage tags.

Both are kind 32217 with the same blob mechanics as A1, authored by the retailer.

A4 — Direct ownership purchase [NEW]

Two payment legs, same entitlement outcome. The Cashu leg is normative for the MVP because it matches the working stack; the Lightning leg is the equivalent via NIP-57.

Preconditions: buyer knows the asset address, the owner pubkey, and the price; buyer has funds at a mint the owner accepts (A1 step 2).

Cashu leg

buyer                              relay                      owner sales service
  │ 1. build token (P2PK → K_own)     │                              │
  │ 2. seal+wrap order rumor          │                              │
  │ 3. EVENT [1059 wrap]  ───────────► │ ───────────────────────────► │
  │                                   │ ◄─ OK (accepted/stored)      │
  │                                   │        4. unwrap, validate order + token
  │                                   │        5. POST /v1/swap  (burn proofs, NUT-03)
  │                                   │        6. PUT /upload  (new filter blob)
  │                                   │        7. EVENT [32217] (new x)  ─────► relay
  │ 8. REQ filter / HEAD blob / GET   │                              │
  │    (verify membership)            │                              │
  1. Build the token. The buyer's wallet produces a V4 cashuB token with proofs

summing to ≥ price, P2PK-locked (NUT-11) to K_own, from an accepted mint. The buyer does this before sending anything, so the order is a single self-contained message.

  1. Wrap the order. The order is a rumor, NIP-59 sealed by K_buy and wrapped to

K_own:

{
  "kind": 3222,
  "pubkey": "<buyer>",
  "created_at": 1758130000,
  "content": "",
  "tags": [
    ["a", "32210:<owner>:<track-d>", "wss://relay.musiquay.com"],
    ["p", "<owner>"],
    ["tier", "fan"],
    ["price", "500", "sat"],
    ["payment", "cashu"],
    ["mint", "https://mint.example.com"],
    ["token", "cashuB..."]
  ]
}

kind 3222 is a proposal — the order is never published to a relay, it exists only inside the wrap, exactly like a 32218 receipt. Verify the number against the registry of kinds before implementation (§12 PO-1).

  1. Deliver. ["EVENT", <kind-1059 wrap>] to the owner's read relays (from the owner's

NIP-B7 kind 10063 list and profile relay hints). The wrap's only routing data is ["p", "<owner>"].

  1. Validate. The sales service MUST check, in order: the seal's author is the buyer

claimed in the order; the a tag names an asset whose author is the owner; price ≥ the asset's current price; the token's mint is accepted; the token decodes and amounts sum correctly; the token is not already spent (POST /v1/checkstate or the swap itself). Any failure ⇒ no entitlement; optionally reply with a reason (out of band).

  1. Claim. POST /v1/swap with the token's proofs as inputs and fresh blinded outputs

P2PK-locked to K_own. The response MUST be decoded and unblinded; a swap whose outputs are discarded burns the payment. (§12 G-7)

  1. Record. Append the buyer pubkey to the retained element set; publish the new blob

(A1 steps 3–4).

  1. Republish. Publish the replacement 32217 with the new x, same d, later

created_at. Delete the superseded filter blob with BUD-12 (DELETE /<old-x> + BUD-11 t=delete token).

  1. Verify (buyer). No confirmation event is required. The buyer either fetches the

replacement filter and tests membership locally, or simply proceeds to the download (A9). The filter is the source of truth, not a receipt.

Lightning leg (NIP-57)

  1. Buyer builds a zap request (kind 9734) for the owner, with amount = price and the

asset a tag; sends it to the owner's lud16 LNURL callback.

  1. LNURL server returns a BOLT-11 invoice; buyer pays it.
  2. LNURL server publishes the zap receipt (kind 9735), which is signed by the LNURL

server's key and embeds the original 9734.

  1. The sales service watches for 9735 events addressed to K_own, verifies the embedded

request and the invoice, and proceeds at step 6 above.

Entitlement is identical; only the payment proof differs. The buyer's pubkey comes from the zap request's author, so NIP-57 gives the entitlement step for free.

A5 — Retailer ownership purchase [SPEC] + [NEW]

The owner's fee is always the content revenue; the retailer takes no cut of it. Two models:

Forward model — the retailer is only a storefront.

  1. Buyer sends the order (A4) wrapped to the retailer, with tier = retail.
  2. The retailer re-wraps the order to the owner, or instructs the buyer to send it directly.
  3. The owner adds the buyer to the fan filter. Everything else is A4.

On-behalf model — the retailer collects on the owner's behalf.

  1. Buyer sends the A4 order to the retailer with tier = retail.
  2. The retailer swaps the token at the mint to outputs P2PK-locked to `K_own` (not to

itself), i.e. it forwards the exact amount.

  1. The retailer adds the buyer to its retail:<asset-address> filter and republishes it.
  2. The buyer downloads; hosts accept either the fan filter or any retailer per-asset filter

whose retailer is listed on the owner's fan filter (A10).

Trust boundary. In the on-behalf model the retailer holds a token locked to the owner and is trusted to forward it. The protocol cannot enforce forwarding; hosts and clients can only observe that the buyer is entitled. Where this matters, use the forward model or the owner's own checkout.

A6 — Subscription [NEW]

  1. Buyer and retailer agree a channel and a period (out of band or from the retailer's

listing). Buyer sends an A4-shaped order wrapped to the retailer with tier = sub, ["channel","<channel-d>"], ["period","<seconds>"], and a token locked to K_ret.

  1. Retailer swaps the token to itself, then rebuilds its subscription filter element set to

the currently active subscribers (including this buyer) and republishes d = sub:<retailer>:<channel-d>.

  1. The subscription filter MAY carry ["catalog","<asset-address>",...] coverage tags. If

it does not, the host resolves coverage from the owners' retailer tags plus the retailer's catalog.

  1. While subscribed, the buyer streams covered assets — the host treats them as subscription

serves (A10 step 4).

  1. Renewal is a new order before expiry. No per-period event is published; the republished

filter is the membership state.

A7 — Lapse and rebuild [SPEC]

The retailer rebuilds the subscriber set on each renewal cycle and drops lapsed pubkeys. Hosts cache the filter and replace it when a newer version arrives at the address. Because hosts are expected to hold a subscription to each filter address they honour, invalidation is push-driven, not polling.

A8 — Revocation [NEW]

owner/retailer                     host (subscribed)          relay
   │ 1. rebuild blob without <pk>       │                       │
   │ 2. PUT /upload (new blob)          │                       │
   │ 3. EVENT [32217] new x  ──────────►│◄──── EVENT 32217 ─────│
   │    DELETE /<old-x>  ──────────────►│                       │

fp = 1e-9 this is negligible; at the maximum permitted fp = 1e-6 it is a known, accepted residual risk.

rentals.

A9 — Entitled re-download [SPEC]

  1. Buyer builds a BUD-11 token: kind 24242, t = get, ["x","<blob-sha256>"],

["expiration","<now+300>"], content explaining the use; signs with K_buy; base64url.

  1. GET /<sha256> with Authorization: Nostr <token> (optionally Range).
  2. Host runs the gating decision (A10).
  3. On 200, the host issues a receipt (D1) asynchronously. Re-downloads are unlimited for

as long as membership holds; no token or payment is presented on the request itself.

A10 — Host gating decision [SPEC] + [NEW]

The host MUST resolve the request to an asset before it can check any filter.

GET /<sha256> [Authorization: Nostr <24242 t=get, x=sha256>]
  │
  ├─ 0. Resolve blob → asset
  │     local index built at ingest: sha256 → asset address
  │     (see G-1 for the on-chain query fallback)
  │     unknown blob → treat as un-gated storage fetch (host policy)
  │
  ├─ 1. No 32217 exists for the asset                     → serve, free, receipt
  │
  ├─ 2. No Authorization header, asset is gated           → 401  WWW-Authenticate: Nostr
  │
  ├─ 3. buyer ∈ owner fan filter                          → serve, receipt (ownership)
  │
  ├─ 4. for each ["retailer", R] on the owner's filter:
  │       buyer ∈ R's retail:<asset> filter               → serve, receipt (ownership)
  │       buyer ∈ R's sub:<R>:<channel> filter
  │         and asset ∈ R's covered catalog               → serve, receipt (subscription)
  │
  └─ 5. otherwise                                         → 402 + X-Cashu (NUT-24)

Details that implementations get wrong:

402 is the challenge for a known buyer without entitlement (rental) or a bytes fee.

event at the address invalidates the cache. The host SHOULD verify the blob's sha256 against the event's x before trusting it.

bytes → header magic/version/asset address match the event and the requested asset → parameters (fp, n, k) consistent with the bit-array length.

certifiable.

A11 — Host onboarding (filter federation) [SPEC]

A host that agrees to the terms serves without contacting the owner:

  1. Discover: the host indexes assets it stores and their filter event addresses.
  2. `

["REQ","onboard",{"kinds":[32217],"authors":["<owner>"],"#d":["fan:<asset-address>"]}] `

  1. Verify the owner's signature on the event.
  2. Fetch the blob from the server hint, falling back to the owner's NIP-B7 kind 10063

server list; verify sha256(bytes) == x.

  1. Mirror the blob if desired (PUT /mirror, BUD-04) so gating survives the origin host.
  2. Subscribe to the filter address for replacement, and cache the element set.

A12 — Superseded filter cleanup [SPEC]

On observing a new version at a filter address, a host SHOULD DELETE /<old-x> (BUD-12, BUD-11 t = delete, x = old-x) to bound Blossom storage. Superseded filters have no archival value; the receipt archive is the historical record, not the filter.

A13 — Rental stream bucket [SPEC]

listener                                 host
  │ GET /<sha256>  (no auth or auth, not entitled)
  │ ─────────────────────────────────────►│  gate: no filter membership
  │ ◄── 402 X-Cashu: creqA… (bucket = 600s)│
  │ (wallet decodes creqA, picks a listed mint, builds token)
  │ GET /<sha256>  X-Cashu: cashuB…        │
  │ ─────────────────────────────────────►│  validate + swap, open 600s window
  │ ◄── 200 + bytes (Range supported)      │
  │    … stream within the window …        │
  │ ◄── 32218 receipt (mode=stream, start/end/amount_sats)

bucket.

A14 — Bytes fee [SPEC]

A host MAY charge for bytes independently of entitlement, expressed through the same NUT-24 mechanism. It does not affect filter membership: an entitled buyer can still be charged a bytes fee if that is the host's policy, and the receipt records the settled amount. Entitlement and hosting economics are separate axes.

7. Delivery and Settlement Flows

D1 — Issue a delivery receipt [SPEC]

Triggered by every serve — free, owned, subscription, rental, bytes-fee.

  1. Host builds the receipt rumor (unsigned), kind 32218:
{
  "kind": 32218,
  "pubkey": "<host>",
  "created_at": 1758130000,
  "content": "",
  "tags": [
    ["a", "32210:<owner>:<track-d>"],
    ["p", "<buyer>"],
    ["x", "<blob-sha256>"],
    ["server", "https://blossom.musiquay.com"],
    ["mode", "download"],
    ["start", "1758130000"],
    ["end", "1758130000"],
    ["bytes", "48216387"],
    ["amount_sats", "0"],
    ["paid", "<token-hash>"]
  ]
}
  1. Seal (kind 13) and wrap (kind 1059) it once to the buyer and **once to the

publisher** (two wraps, one rumor each).

  1. Publish each wrap to the recipient's read relays.
  2. The publisher archives every receipt for accounting. The buyer keeps theirs as proof of

purchase.

Rules:

Relays SHOULD only serve 1059 to the p-tagged recipient and SHOULD require AUTH.

D2 — Publish a host aggregate [SPEC]

Per asset, per period the host chooses:

{
  "kind": 32219,
  "pubkey": "<host>",
  "content": "",
  "tags": [
    ["a", "32210:<owner>:<track-d>"],
    ["start", "1758130000"],
    ["end", "1760808400"],
    ["plays", "48213"],
    ["downloads", "3921"],
    ["bytes", "21474836480"],
    ["sats", "1510000"],
    ["listeners", "11054", "approx"]
  ]
}

attributable.

an estimate.

reports.

D3 — Publish a publisher rollup [SPEC]

The publisher consolidates host aggregates by reference, preserving each host's signature:

{
  "kind": 32220,
  "pubkey": "<publisher>",
  "content": "",
  "tags": [
    ["a", "32210:<publisher>:<track-d>"],
    ["start", "1758130000"],
    ["end", "1760808400"],
    ["e", "<host-aggregate-id>", "", "<host-pubkey>"],
    ["e", "<host-aggregate-id-2>", "", "<host-2-pubkey>"],
    ["receipts", "52134"],
    ["plays", "55123"],
    ["downloads", "4120"],
    ["bytes", "22817013760"],
    ["sats", "1730000"]
  ]
}

independently, then the publisher's signed pointer-set.

survives.

D4 — Receipt archive and recompute [NEW]

The publisher's archive is the durable record. A conforming publisher:

  1. Subscribes to its own receipts:

` ["REQ","receipts",{"kinds":[1059],"#p":["<publisher>"]}] ` (requires AUTH on relays that gate wraps).

  1. Stores each unwrapped 32218 keyed by (asset, host, period).
  2. Recomputes and re-publishes 32220 rollups whenever aggregates arrive or the archive

changes. Signatures on the referenced aggregates survive re-publication.

8. Play Reporting and Certification

Play reports are the listener-side public signal. They are regular, immutable, and counted with NIP-45. They are not the payment record — receipts and aggregates are.

R1 — Free play report [SPEC]

  1. Listener plays a free asset.
  2. Publish kind 3221:
{
  "kind": 3221,
  "pubkey": "<listener>",
  "content": "",
  "tags": [
    ["a", "32210:<publisher>:<track-d>", "wss://relay.musiquay.com"],
    ["p", "<artist-pubkey>"],
    ["x", "<blob-sha256-played>"],
    ["server", "https://blossom.musiquay.com"],
    ["codec", "mp3-128kbps-VBR"],
    ["t0", "1758130000"],
    ["t1", "1758130247"]
  ]
}

Free plays are self-attested: signal, not proof. No blind, host, or statement tags.

R2 — Paid play report with a blind signature [OPEN]

The intent: the host certifies the play without learning which listener published it.

Statement. Canonical string, hashed-to-curve as the BDHKE secret:

musiquay.play:v1|<asset-address>|<blob-sha256>|<start>|<end>|<amount_sats>|<unit>

Everything is fixed-width-free but unambiguous: fields are joined by |, no field may contain |.

Signing request. [NEW] The host exposes:

POST /blindsign
{ "B_": "<33-byte compressed hex>" }          # B_ = Y + rG, Y = hash_to_curve(statement)
→ { "C_": "<33-byte compressed hex>",
    "dleq": { "e": "<hex>", "s": "<hex>" } }  # NUT-12 proof

The host MUST only blind-sign within an authenticated session it actually served, MUST rate-limit, and SHOULD NOT retain requests.

Unblind and publish. The listener unblinds C = C_ − rK, then publishes:

{
  "kind": 3221,
  "pubkey": "<listener>",
  "content": "",
  "tags": [
    ["a", "32210:<publisher>:<track-d>"],
    ["p", "<artist-pubkey>"],
    ["x", "<blob-sha256-played>"],
    ["server", "https://blossom.musiquay.com"],
    ["codec", "mp3-128kbps-VBR"],
    ["t0", "1758130000"],
    ["t1", "1758130247"],
    ["blind", "<C hex>"],
    ["host", "<host-pubkey>"],
    ["statement", "<canonical statement hex>"],
    ["dleq", "<e hex>", "<s hex>"]
  ]
}

Open problems, both requiring a decision before this flow is implementable:

not computable. Verification requires the NUT-12 DLEQ proof, or the mint/host as verifier. The current spec text is wrong on this point (§12 G-3).

statement can be made to certify false statements. The mitigation is to restrict the signed statement to facts the host itself observed (asset, blob, window) and to leave tier/amount out of it — money is accounted by receipts and aggregates, not by play reports.

R3 — Now-playing status [SPEC]

The live social layer is separate from the record: NIP-38 kind 30315, d = music, expiring when the track ends. It is addressable and ephemeral in effect — a status, not a play. Clients MUST NOT count 30315 toward play totals.

9. Scene Pillar Flows

These are mostly NIP reuse. The flows are listed so the sequencing and the tag conventions are unambiguous; the roles and vocabulary live in the feature docs.

S1 — Calendar event and RSVP [SPEC]

creator                              relay                       attendee
  │ EVENT [31923]  ─────────────────► │
  │ ◄── OK                            │
  │                                   │ ◄──── REQ {"kinds":[31923],"#g":["eyckch"]} ────│
  │                                   │ ───── EVENT 31923 … EOSE ──────────────────────►│
  │                                   │ ◄──── EVENT [31925] RSVP ──────────────────────│

NIP-52**, see G-8), start_tzid, location, g, p tags with roles, t, r. Musiquay adds ["price","10","EUR"] and capacity conventions (G-11).

(accepted|declined|tentative), fb (free|busy), optional p = creator.

filter precisely client-side. g is single-letter and therefore indexed.

S2 — Waitlist join and promotion [OPEN]

  1. Join. Publish 31925 with ["status","waitlist"], ["position","<n>"] (informational),

["joined_at","<unix>"], unique d.

  1. Observe. Anyone may query the whole queue:

` ["REQ","waitlist",{"kinds":[31925],"#a":["<event-address>"]}] ` Queue order is joined_at ascending, tie-broken by d ascending. position is a hint; clients recompute.

  1. Promote. When capacity frees, the creator promotes the head of the queue by DM and

the attendee replaces their own RSVP (status = accepted, same d).

  1. Gap. NIP-52 has no capacity and no authoritative queue head. Promotion is therefore a

social action, not a protocol transition. See G-11.

S3 — Ticket pre-sale [OPEN]

Two halves: who may buy, and what a ticket is.

Access tiers. A pre-sale window is an access filter, reusing kind 32217:

d = presale:<event-address>:<tier>        tier ∈ supporter | community | general
element set = pubkeys admitted to this window

The seller's checkout checks the buyer's membership in the current window's filter. Because relays serve all signed events to anyone, the window is enforced at purchase time, not by concealing the listing.

Ticket credential. Three options, all buildable with the current stack:

OptionMechanismIdentity-boundDouble-entryUnlinkable
Cashu ticket (recommended)P2PK token (NUT-11) locked to the buyer, issued from a seller swapYes — holder must sign a P2PK witness at the doorYes — check-in burns the token at the mintNo
Blind-signed ticketBDHKE signature over an event/tier statementNoNo (a copy is indistinguishable)Yes
Wrapped deedNIP-59 ticket with a venue-signed entitlementYes (DM key)NoPartial

Check-in with the Cashu ticket: the door verifies the P2PK witness against the buyer's key, then POST /v1/swap (or a mint-supported burn) to make the token unusable. That gives anti-scalping and double-entry prevention from primitives that already exist.

Flow: buyer sends an A4-shaped order with tier = ticket and the event address; pays the seller's invoice (Lightning or Cashu); the seller swaps to outputs P2PK-locked to the buyer's pubkey and delivers the token in a NIP-59 wrap; the ticket is the token.

S4 — Publish the roster [SPEC]

The post-show roster is the calendar event, replaced.

  1. Creator re-publishes 31923 at the same d, keeping all calendar fields, with the

complete p roster (p = pubkey, relay hint, role from the roster vocabulary).

  1. There is no a tag pointing at a separate roster — the address is the event address.
  2. Order matters only for display; the roster has no positional semantics.

S5 — Confirm, deny, correct [SPEC]

A participant publishes NIP-32 kind 1985:

{
  "kind": 1985,
  "pubkey": "<participant>",
  "content": "optional comment",
  "tags": [
    ["a", "31923:<creator>:<event-d>", "wss://relay.musiquay.com"],
    ["p", "<participant>"],
    ["L", "musiquay.credit"],
    ["l", "confirmed", "musiquay.credit"],
    ["role", "sound-foh"]
  ]
}

given (signer, target, role) the latest `created_at` wins (ties broken by lowest id); earlier ones remain visible as history.

S6 — Attest [SPEC]

Same kind, different author and namespace:

["a", "31923:<creator>:<event-d>"], ["p", "<vouched-for>"],
["L", "musiquay.attest"], ["l", "attests", "musiquay.attest"], ["role", "sound-foh"]

Weight comes from the attestor's own position in the scene graph, not from the protocol.

S7 — Wiki contribution [SPEC]

NIP-22 kind 1111 scoped to the roster address:

{
  "kind": 1111,
  "pubkey": "<contributor>",
  "content": "Adding the lighting tech.",
  "tags": [
    ["A", "31923:<creator>:<event-d>", "wss://relay.musiquay.com"],
    ["K", "31923"],
    ["P", "<creator>"],
    ["a", "31923:<creator>:<event-d>", "wss://relay.musiquay.com"],
    ["k", "31923"],
    ["p", "<creator>"],
    ["p", "<missing-participant>", "wss://relay.musiquay.com"],
    ["role", "lighting"]
  ]
}

The contributed participant then goes through S5/S6. A contribution is a suggestion; the original roster is never overwritten.

S8 — Setlist [SPEC] + [NEW]

Publish 32213 with a = roster address, p = performing artist, title, ordered song tags, encore markers, t = setlist.

["song", "5", "Song Title Five", "32210:<pub>:<d>", "feat", "<guest-pubkey>"]

[NEW] The song tag is multi-letter and therefore not indexed by relays. For "performed live N times" queries to work without a client-side index, the setlist SHOULD also carry one single-letter ["a","32210:<pub>:<d>"] tag per published song, in set order, so {"#a":[track-address]} works. See G-9.

S9 — Credits and editions [SPEC]

  1. Release-level credits: 32214 with d, a = album address, and p-with-role tags.
  2. Per-track credits: 32215 with d, a = track address and a = the 32214

address. Inheritance: a track with no 32215 inherits the release credits entirely; a 32215 overrides only the roles it states.

  1. Editions: 32216 with d, a = album address, format/edition tags, i/k external

IDs, and edition-specific p roles.

  1. Validation is S5/S6/S7 applied to these addresses instead of a roster — the same kinds,

the same namespaces.

S10 — Merch storefront and order [SPEC]

  1. Stall — 30017, content = NIP-15 JSON (id, name, currency, shipping).
  2. Product — 30018, content = NIP-15 JSON (id, stall_id, name, price, quantity,

specs, shipping extras). Physical music links its edition via the product specs.

  1. Order — NIP-15 checkout is JSON in a kind 4 direct message:

{"id","type":0,"items":[{"product_id","quantity"}],"shipping_id","contact":{…}}.

  1. Payment request — merchant replies type: 1 with payment_options

(ln, lnurl, btc, url).

  1. Status — merchant replies type: 2 with paid and shipped.

[IMPL]/[OPEN] NIP-15 specifies NIP-04, which is deprecated, and the musiquay reference stack carries no NIP-04 or NIP-17 — its only DM transport is MLS (Marmot). Either NIP-15 checkout is re-encoded over NIP-17, or over MLS, and the choice recorded. See G-12.

Shipping and fulfillment stay off-protocol: the seller ships, the platform does not mediate.

S11 — Resale and services listing [SPEC]

  1. Publish 30402 with d, title, summary, published_at, price

(["price","500","EUR","day"] for frequency-priced services), location, g, t, image, status.

  1. Drafts are 30403 with identical structure; publishing means emitting 30402.
  2. Contact is a DM. No escrow, no dispute mediation, no rating system: reputation is the

scene graph (S9 + S4 history).

  1. Resale of physical music carries the edition reference so provenance is visible from the

listing.

10. Operational Flows

O1 — Relay auth for gated writes [SPEC]

relay:  ["AUTH", "<challenge>"]
client: ["EVENT", <event>]
relay:  ["OK", "<id>", false, "auth-required: only registered keys may write"]
client: ["AUTH", <kind 22242, ["relay",…], ["challenge",…]>]
relay:  ["OK", "<auth-id>", true, ""]
client: ["EVENT", <event>]           # retry the same event
relay:  ["OK", "<id>", true, ""]

authenticated. Clients MUST NOT retry forever on restricted:.

serving 1059 events, and SHOULD serve them only to the p-tagged recipient.

O2 — Replication and mirroring [SPEC]

readable from any Nostr client (orly uses NIP-77 negentropy for set reconciliation). Independent operators (festivals, labels) run their own relays with their own keys.

host pull a blob it does not have. Filter blobs mirror like any other blob, which is what keeps gating independent of the origin host (A11).

relay. Persistence is a service, not a protocol guarantee.

O3 — Deletion and vanish [SPEC]

plus k (target kind). Best-effort: relays SHOULD honour it, replicated copies elsewhere may persist.

server and x. Unscoped delete tokens are dangerous — always scope them.

blob effectively withdraws the asset from hosts that gate on it. The filter event should be deleted too (NIP-09 a tag), otherwise hosts retry a dead blob.

until the owner rebuilds it (A8). Decide the policy per asset — withdrawal vs refund.

O4 — Recovery and restore [NEW]

Lost thingRecovery
Cashu wallet stateNUT-09 restore from the BIP-39 mnemonic; NUT-13 deterministic secrets mean proofs are re-derivable.
Pending swapNUT-07 POST /v1/checkstate for the input proofs; NUT-19 makes retrying a mint/swap with the same payload safe.
Filter element setOwner-local responsibility. Without the retained set, only the current filter blob is recoverable, and revocation/appends become impossible (A1). Owners SHOULD back it up.
ReceiptsRe-REQ the wraps: {"kinds":[1059],"#p":["<publisher>"]} on each relay; the union of relays is the best available archive.
Events generallyRe-REQ from any relay holding them. Addressable events return the latest version only; history is not recoverable from relays.

11. Cross-Cutting Invariants

11.1 Verification order

Every consumer verifies in this order, and stops on the first failure:

  1. Event id = sha256 of the NIP-01 serialisation; sig valid for pubkey.
  2. Address (a tag) parses as kind:pubkey:d, and the kind is the expected one.
  3. Referenced signer is who the flow says it is (owner signs filters, host signs receipts and

aggregates, publisher signs rollups, the participant signs their own confirmation).

  1. Blob bytes hash to the x value they claim.
  2. Payment proof: token decodes, unit matches, amount sufficient, proofs unspent, mint

accepted.

  1. Only then apply business logic.

11.2 Ordering and replacement

periods are new events).

the x tag changing in isolation. An event whose x does not match its blob is invalid.

11.3 Idempotency

OperationIdempotentNotes
PUT /uploadYes200 vs 201 distinguishes existing from new; bytes are content-addressed.
Bloom insertYesSetting bits twice is a no-op.
Filter republishYesSame element set ⇒ same blob ⇒ same x.
POST /v1/swapOnly with NUT-19Same payload may be served from cache; do not build new outputs per retry.
Receipt issuanceYesRe-serving the same buyer for the same blob produces a new deed; that is correct, not a duplicate.
Play report publishYesDuplicate reports from one key inflate counts; clients SHOULD NOT re-publish on retry.

11.4 Time

relays and authoritative only where a flow says so (subscription filters, tokens, BUD-11 auth tokens).

11.5 Privacy

breaks the privacy of the deed; hence AUTH-gated wrap delivery.

information at the bit level; do not describe it as private.

11.6 Failure modes

SignalMeaningCorrect reaction
["OK", id, false, "duplicate: …"]Relay already has itTreat as success.
["OK", id, false, "invalid: …"]Malformed/verification failureFix, do not blind-retry.
["OK", id, false, "rate-limited: …"]Back offRetry with delay.
["CLOSED", sub, "auth-required: …"]Need NIP-42Run O1, then re-REQ.
400 + X-ReasonBad/expired/insufficient payment proofRebuild the proof; do not retry as-is.
401 from BlossomNo/invalid BUD-11 tokenSign a fresh 24242 token and retry.
402 from BlossomPayment requiredPay the X-Cashu/X-Lightning challenge, retry the GET/PUT (never HEAD).
404Blob or filter absentFall back to the server hint, then the owner's 10063 list.
409 from BlossomX-SHA-256 mismatchBug; do not retry.
swap rejected by mintSpent/invalid proofsThe payment did not happen; no entitlement.
no wrap arrivesDelivery failureRe-REQ later; the publisher archive is authoritative. Filter membership is still checkable directly.

12. Gaps and Open Decisions

Each item was a decision that had to be made before the flow it blocks could be implemented. Items in 12.1 have been folded into the source documents; the items in 12.2 are still open, with the recorded recommendation.

12.1 Applied

PO-1 — Purchase order kind and channel. [APPLIED] A4/A5/A6 now use a NIP-59-wrapped order rumor, kind 3222, never published, with NIP-57 as the Lightning leg. 3222 was re-checked free in the registry of kinds on 2026-09-28 and is in the ARCHITECTURE.md registry and FEATURES_ACCESS.md. An HTTP purchase endpoint with a NUT-24 handshake remains a permitted alternative for hosts that want synchronous checkout.

G-1 — Blob → asset resolution. [APPLIED] Track events now carry one single-letter ["x", sha256] tag per encoding (FEATURES_MUSIC.md), so {"kinds":[32210],"#x":["<sha256>"]} is the standard lookup. Hosts SHOULD also keep an ingest-time index as a cache.

G-2 — 401 semantics for HTTP. [APPLIED] FEATURES_ACCESS.md now specifies BUD-11 (kind 24242, Authorization: Nostr …) as the identity mechanism, with 401 before 402 in the gating order. NIP-42 is relay auth only.

G-3 — Blind-signature verification and oracle risk. [APPLIED] Play reports now carry a dleq tag, the statement is restricted to host-observed facts (musiquay.play:v1|<asset>|<blob>|<start>|<end>), and the pair-check claim is corrected in both FEATURES_ACCESS.md and FEATURES_MUSIC.md.

G-6 — Mint and payment-leg discovery. [APPLIED] The 32217 event now carries mint tags (including ["mint","lightning"]). The gonuts NUT-18/26 gap is recorded in LAB_BUD07.md.

G-8 — NIP-52 `D` tag missing. [APPLIED] Added to the calendar and roster 31923 examples, with the derivation rule.

G-9 — Setlist songs are not queryable. [APPLIED] Setlists now carry one single-letter ["a", track-address] tag per published song, in set order.

G-11 — Capacity and queue authority. [APPLIED] FEATURES_CALENDAR.md now records the ["capacity","<n>"] convention and that promotion is a creator DM to the head of the recomputed queue.

G-13 — Subscription catalog coverage. [APPLIED] The retailer's catalog is now authoritative; retailer tags are authorization, not coverage.

G-14 — Free-with-tip vs free assets. [APPLIED] FEATURES_ACCESS.md now has a Tips rule: a tip is a payment leg recorded on the receipt and never changes entitlement.

G-16 — Filter blob format versioning. [APPLIED] Fail closed on an unparseable filter for a priced asset; free means "no filter event exists".

12.2 Still open

G-4 — Album-level entitlement. A price on an album (kind 32211) has no defined gating behaviour, because audio lives on tracks and filters are per asset. Options: (a) an album purchase adds the buyer to each track's fan filter (many republishes, simple gating); (b) an album-level filter that hosts consult after resolving blob → track → album. Recommendation: (b). Recorded as a proposal in FEATURES_ACCESS.md; until settled, hosts treat album price tags as informational.

G-5 — Receipt transport vs client capability. Receipts are delivered as NIP-59 wraps, but the musiquay reference stack has no NIP-59 and uses MLS (Marmot) as its only DM transport. Options: (a) implement NIP-59 in the client; (b) deliver receipts over MLS as an application message; (c) deliver receipts over an authenticated HTTP endpoint, keeping the wrap as the archival copy. Recommendation: decide before building the first client, since it changes the host's delivery path. Until then the publisher archive (D4) is the only durable record.

G-7 — Claiming a payment correctly. The prototype's swapAtMint discards the swap response and the blinding scalars, so artist-routed proofs are unclaimable (LAB_BUD07.md). Any A4/A5 implementation MUST decode the swap response, unblind each output, and store the resulting proofs. Specified; implementation outstanding. This is the single highest-priority fix before a sale is real.

G-10 — Ticket representation. §S3 lays out three options with different anti-scalping and privacy properties. Recommendation: Cashu P2PK ticket — identity-bound and double-entry-proof at the mint, using the existing stack. Recorded as an open decision in FEATURES_CALENDAR.md. Settle before the calendar pillar ships, since it is the only commerce flow with a fraud surface.

G-12 — Merch checkout transport. NIP-15 checkout is JSON in NIP-04 messages, which is deprecated and unavailable in the reference client. FEATURES_MERCHANDISE.md already lists NIP-17/NIP-44 for buyer↔seller DMs, so the payloads need re-homing rather than redesign. Recommendation: carry the same NIP-15 type 0/1/2 payloads over NIP-17, or over MLS for the musiquay-based client, and record which is normative for the MVP.

G-15 — Kind registration and relay policy. 3221, 3222, and 32210–32220 are free in the registry as of 2026-09-28 but are not registered, and a relay's kind allow-list will drop unknown kinds. Recommendation: register the block, then ensure the relay policy whitelists it before any client ships.

13. Conformance Checklist

The minimum a component must do to interoperate. Test vectors should be derived from this list; the acceptance test is that two independent implementations pass them against each other.

Relay

Host

Publisher / Owner

Client

Suggested first end-to-end test

One scripted sequence that exercises the whole core, and nothing else:

  1. Publish a track with a price tag and an empty fan filter (n = 0).
  2. GET the blob unauthenticated → 401; with a valid BUD-11 token → 402.
  3. Buyer sends an A4 order with a token P2PK-locked to the owner.
  4. Owner swaps, unblinds, rebuilds the filter, republishes 32217, deletes the old blob.
  5. Buyer re-fetches the filter, confirms membership, downloads → 200; host issues

receipts to buyer and owner.

  1. Host publishes a 32219 aggregate; owner publishes a 32220 rollup.
  2. Buyer publishes a 3221 play report; relay COUNT includes it.
  3. Revoke the buyer (A8); a fresh GET → 402 again.

That sequence is the specification's proof of life: everything in §5–§8 that is not scene graph is on the path.