# Musiquay — Protocol Message Flows > [ARCHITECTURE.md](./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: - the transports and message primitives the flows are built from (§3), - publishing and catalog flows (§5), - paid access: filters, purchase, gating, rental (§6), - delivery settlement: receipts, aggregates, rollups (§7), - play reporting and paid-play certification (§8), - the scene pillars, where flows are mostly NIP reuse (§9), - operational flows: auth, replication, deletion, recovery (§10), - invariants that hold across all flows (§11), - gaps and decisions that must be made before implementation (§12). **Status markers** are used on every flow: | Marker | Meaning | |--------|---------| | `[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](./VISION.md) and [FEATURES_ROSTER.md](./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. | Actor | Key | Role in flows | |-------|-----|---------------| | **Publisher / Owner** | `K_own` | Author of the track/album event. Sets `price`, authors access filters, receives the ownership fee, archives receipts, publishes rollups. | | **Artist** | `K_art` | Tagged participant (`p` tag), not necessarily the author. Confirms credits. | | **Host** | `K_host` | Blossom server that stores and serves blobs. Verifies filters, gates access, issues receipts, signs plays, publishes aggregates. | | **Retailer** | `K_ret` | Operates 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 / Listener** | `K_buy` | Pays, joins filters, downloads, streams, publishes play reports. | | **Relay** | — | Stores and serves events (NIP-01). May require AUTH (NIP-42). | | **Mint** | `K_mint` | Cashu mint: issues and redeems ecash (NUT-00..26). | | **Lightning node / LNURL** | `K_ln` | BOLT-11 settlement; NIP-57 zaps for the Lightning payment leg. | | **Sales service** | `K_own` or delegated | Watches 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. | | **Client** | `K_usr` | Any 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: | Message | Form | Use | |---------|------|-----| | `EVENT` | `["EVENT", ]` | Publish any signed event. | | `REQ` | `["REQ", , , ...]` | Subscribe to stored + live events. | | `CLOSE` | `["CLOSE", ]` | End a subscription. | | `COUNT` | `["COUNT", , , ...]` | Count matching events (NIP-45). | | `AUTH` | `["AUTH", ]` | Answer a relay challenge (NIP-42). | Relay → client: | Message | Form | Use | |---------|------|-----| | `EVENT` | `["EVENT", , ]` | Stored or live event. | | `EOSE` | `["EOSE", ]` | End of stored events; live tail follows. | | `OK` | `["OK", , , ": "]` | Publish result; the only publish acknowledgement. | | `CLOSED` | `["CLOSED", , ": "]` | Subscription refused or killed; also used when `COUNT` is refused. | | `NOTICE` | `["NOTICE", ""]` | Human-readable diagnostic. | | `COUNT` | `["COUNT", , {"count": }]` | Count result (may include `"approximate": true` and/or `"hll"`). | | `AUTH` | `["AUTH", ""]` | Relay requires authentication. | Rules that the flows depend on: - An addressable event (`30000 ≤ kind < 40000`) is identified by `kind:pubkey:d`, not by 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. - Filter lists for `ids`, `authors`, `#e`, `#p` MUST be exact 64-character lowercase hex. - **Only single-letter tags are indexed.** `{"#x": [...]}` works; `{"#audio": [...]}` does not. This constrains asset/blob lookup — see §12 G-1. - Multiple filters in one `REQ` are OR'd; all conditions inside one filter are AND'd. - On an addressable kind, a relay serves the latest version per `(kind, pubkey, d)`; 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). | Endpoint | BUD | Purpose | |----------|-----|---------| | `GET /[.ext]` | 01 | Fetch a blob. Supports `Range` (`206`). | | `HEAD /[.ext]` | 01 | Existence, `Content-Type`, `Content-Length`, `Accept-Ranges`. | | `PUT /upload` | 02 | Upload. `201` new, `200` already present, body is a **blob descriptor**. | | `HEAD /upload` | 06 | Pre-flight with `X-SHA-256`, `X-Content-Length`, `X-Content-Type`. | | `PUT /mirror` | 04 | Ask a server to mirror a blob from a URL. | | `PUT /media`, `HEAD /media` | 05 | Media optimization (variants). | | `DELETE /` | 12 | Delete a blob. | | `GET /list/` | 12 | List a pubkey's blobs (unrecommended). | | `PUT /report` | 09 | Report a blob. | Authorization (BUD-11): a signed **kind 24242** event, base64url-encoded without padding, presented as ``` Authorization: Nostr ``` 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](./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: X-Lightning: # optional alternative ``` - `HEAD` responses to a 402 MUST NOT be retried with payment proof; the client pays and retries the `GET`/`PUT`. - The client retries the **same** request with `X-Cashu: cashuB...` (or `X-Lightning: `) and no other change. - Invalid, expired, or insufficient proof ⇒ `400` + `X-Reason`. ### 3.3 Cashu (NUTs) Mint endpoints used by these flows (paths as implemented by `gonuts`): | Method + path | NUT | Purpose | |---------------|-----|---------| | `GET /v1/info` | 06 | Mint info, supported NUTs and units. | | `GET /v1/keys`, `GET /v1/keysets` | 01/02 | Keysets and mint public keys. | | `POST /v1/mint/quote/bolt11` | 04 | Request a mint quote (BOLT-11). | | `GET /v1/mint/quote/bolt11/` | 04 | Poll quote state. | | `POST /v1/mint/bolt11` | 04 | Redeem a paid quote for blinded signatures. | | `POST /v1/swap` | 03 | Redeem/burn proofs, receive new ones. This is also how a seller claims a payment. | | `POST /v1/checkstate` | 07 | Proof state (`UNSPENT`/`SPENT`/`PENDING`). | | `GET /v1/ws` | 17 | WebSocket notifications: `bolt11_mint_quote`, `proof_state`. | Relevant primitives: - **Token formats:** V3 `cashuA` (JSON+base64), V4 `cashuB` (CBOR+base64url). V4 tokens may carry a memo `m`. New flows use V4. - **BDHKE (NUT-00):** wallet picks secret `s`, `Y = hash_to_curve(s)`, blinding scalar `r`, `B_ = Y + rG`; mint returns `C_ = kB_`; wallet unblinds `C = C_ − rK = kY`. - **DLEQ (NUT-12):** the only way a third party can verify a blind signature without the signing key. See §8.2 — the current play-report design omits this and is therefore unverifiable (§12 G-3). - **P2PK (NUT-11):** outputs locked to a pubkey; used for seller payouts and for identity-bound tickets. - **NUT-18/26 payment requests:** `creqA` / `creqB` encodings. NUT-24 requires both. - **NUT-19:** cached responses; safe retry of `mint`/`swap` with the same payload. ### 3.4 Private delivery (NIP-59 + NIP-44) Three layers: | Layer | Kind | Signed by | Visible | |-------|------|-----------|---------| | rumor | any kind, **unsigned** | — | nothing (deniable) | | seal | `13` | real author | author pubkey only | | gift wrap | `1059` (stored) / `21059` (ephemeral) | random one-time key | recipient `p` tag only | Each wrap carries exactly one `["p", ]`. 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 | Object | Address or id | Notes | |--------|---------------|-------| | Track | `a: 32210::` | `d` SHOULD be the ISRC when one exists. | | Album | `a: 32211::` | `d` SHOULD be the MusicBrainz release-group MBID. | | Playlist | `a: 32212::` | NIP-51 set conventions. | | Setlist | `a: 32213::` | References the roster address. | | Release/track credits | `a: 32214:…`, `a: 32215:…` | Track credits reference both a track and the release credits. | | Edition | `a: 32216::` | References the album address. | | Access filter | `a: 32217::` where `d` is `:` | `tier` ∈ `fan` \| `retail` \| `sub`. | | Play report | `e: ` | Regular; `d`-less; counted with NIP-45. | | Delivery receipt | `e: ` | Never public; exists only inside wraps. | | Host aggregate | `e: ` | Regular, public. | | Publisher rollup | `e: ` | Regular, public; references aggregates by `e`. | Filter `d` values, exactly: ``` fan: e.g. fan:32210:: retail: authored by the retailer sub:: authored by the retailer ``` An **asset address** is always a full `::` string, so a `d` value contains colons. That is intentional and matches NIP-01 (`d` is opaque). ## 4. Flow Index | # | Flow | Actors | Transport | Status | |---|------|--------|-----------|--------| | P1 | Upload a blob | client → host | Blossom | `[SPEC]` | | P2 | Publish a track | publisher → relay | Nostr | `[SPEC]` | | P3 | Add an encoding | publisher → host, relay | Blossom + Nostr | `[SPEC]` | | P4 | Publish an album | publisher → relay | Nostr | `[SPEC]` | | P5 | Publish a playlist | curator → relay | Nostr | `[SPEC]` | | P6 | Catalog bootstrap | client → relay | Nostr | `[NEW]` | | A1 | Publish price and fan filter | owner → relay, host | Blossom + Nostr | `[SPEC]` | | A2 | Authorize a retailer | owner → relay | Nostr | `[SPEC]` | | A3 | Retailer filters | retailer → relay | Blossom + Nostr | `[SPEC]` | | A4 | Direct ownership purchase | buyer ↔ owner | Nostr + Cashu/LN | `[NEW]` | | A5 | Retailer ownership purchase | buyer ↔ retailer ↔ owner | Nostr + Cashu/LN | `[NEW]` | | A6 | Subscription | buyer ↔ retailer | Nostr + Cashu/LN | `[NEW]` | | A7 | Lapse and rebuild | retailer → relay | Nostr | `[SPEC]` | | A8 | Revocation | owner/retailer → relay | Nostr | `[NEW]` | | A9 | Entitled re-download | buyer → host | Blossom | `[SPEC]` | | A10 | Host gating decision | host → relay | Nostr + Blossom | `[SPEC]` | | A11 | Host onboarding (federation) | host → relay, owner | Nostr + Blossom | `[SPEC]` | | A12 | Superseded filter cleanup | host → host | Blossom | `[SPEC]` | | A13 | Rental stream bucket | listener ↔ host | Blossom + Cashu | `[SPEC]` | | A14 | Bytes fee | client ↔ host | Blossom + Cashu | `[SPEC]` | | D1 | Issue a delivery receipt | host → buyer, publisher | NIP-59 | `[SPEC]` | | D2 | Publish a host aggregate | host → relay | Nostr | `[SPEC]` | | D3 | Publish a publisher rollup | publisher → relay | Nostr | `[SPEC]` | | R1 | Free play report | listener → relay | Nostr | `[SPEC]` | | R2 | Paid play report (BDHKE) | listener ↔ host, relay | Blossom + Nostr | `[OPEN]` | | R3 | Now-playing status | listener → relay | Nostr | `[SPEC]` | | S1 | Calendar event + RSVP | creator, attendee → relay | Nostr | `[SPEC]` | | S2 | Waitlist join and promotion | attendee, creator → relay | Nostr (+ DM) | `[OPEN]` | | S3 | Ticket pre-sale | buyer ↔ seller → relay | Nostr + LN | `[OPEN]` | | S4 | Publish the roster | creator → relay | Nostr | `[SPEC]` | | S5 | Confirm, deny, correct | participant → relay | Nostr | `[SPEC]` | | S6 | Attest | witness → relay | Nostr | `[SPEC]` | | S7 | Wiki contribution | contributor → relay | Nostr | `[SPEC]` | | S8 | Setlist | author → relay | Nostr | `[SPEC]` | | S9 | Credits and editions | artist/label → relay | Nostr | `[SPEC]` | | S10 | Merch storefront and order | merchant ↔ customer | Nostr (+ LN) | `[SPEC]` | | S11 | Resale and services listing | lister ↔ respondent | Nostr | `[SPEC]` | | O1 | Relay auth for gated writes | client ↔ relay | Nostr | `[SPEC]` | | O2 | Replication and mirroring | relay ↔ relay, host ↔ host | Nostr + Blossom | `[SPEC]` | | O3 | Deletion and vanish | author → relay, host | Nostr + Blossom | `[SPEC]` | | O4 | Recovery and restore | client → mint, relay | Cashu + 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=>`. 200 ⇒ proceed; 402 ⇒ pay the `X-Cashu` challenge and proceed; 4xx ⇒ stop. 2. `PUT /upload` with the binary body, `Content-Type`, `Content-Length`, `X-SHA-256`, and the same `Authorization` header. 3. 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](./FEATURES_MUSIC.md)) with `d`, metadata tags, one `audio` tag per encoding, exactly one `master` tag, and (for paid assets) a `price` tag. 3. `["EVENT", ]` to each write relay. 4. Relay replies `["OK", , 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", ""]` 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`. 3. 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.** 3. 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. 3. `["EVENT", ]`. ### 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":[]}] # 2. recent releases by followed publishers ["REQ","boot-releases",{"kinds":[32211,32210],"authors":[],"limit":200}] # 3. access filters for anything priced (also used for the free/paid decision) ["REQ","boot-filters",{"kinds":[32217],"authors":[],"limit":500}] # 4. credits and editions for the visible albums (by address, second pass) ["REQ","boot-credits",{"kinds":[32214,32215,32216],"#a":[]}] # 5. play totals, if the client wants counts rather than raw reports ["REQ","boot-aggregates",{"kinds":[32219,32220],"#a":[]}] ``` Then, per visible album, a second pass on its track addresses: ``` ["REQ","boot-tracks",{"kinds":[32210],"#d":[]}] ``` Notes: - `#d` on a `REQ` matches the first value of the `d` tag; combined with `kinds` it resolves 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. - The client MUST `EOSE`-close each bootstrap subscription (`["CLOSE","boot-…"]`) once it has what it needs; a catalog subscription left open is a live feed. - Live updates for artists the user follows are separate long-lived subscriptions; do not 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","","sat"]`. Absent ⇒ free. 2. Optional `[NEW]`: the filter event MAY carry one or more `["mint",""]` tags listing accepted Cashu mints, and MAY carry `["mint","lightning"]` if it accepts Lightning directly (§12 G-6). 3. 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)`. 4. `PUT /upload` the blob (P1). Record the returned `sha256`. 5. Publish kind `32217`: ```json { "kind": 32217, "pubkey": "", "content": "", "tags": [ ["d", "fan:32210::"], ["filter", "bloom"], ["fp", "1e-9"], ["n", "1234"], ["k", "30"], ["x", ""], ["server", "https://blossom.musiquay.com"], ["retailer", ""], ["expiration", "1799999999"] ] } ``` 6. 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-",{"kinds":[32217],"authors":[""],"#d":["fan:"]}] ``` **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",""]` 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: - a **per-asset** filter `d = retail:`, element set = buyers who took ownership through it; - a **subscription** filter `d = sub::`, element set = active subscribers, optionally with `["catalog", "", ...]` 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. 2. **Wrap the order.** The order is a rumor, NIP-59 sealed by `K_buy` and wrapped to `K_own`: ```json { "kind": 3222, "pubkey": "", "created_at": 1758130000, "content": "", "tags": [ ["a", "32210::", "wss://relay.musiquay.com"], ["p", ""], ["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](https://github.com/nostr-protocol/registry-of-kinds) before implementation (§12 PO-1). 3. **Deliver.** `["EVENT", ]` 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", ""]`. 4. **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). 5. **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) 6. **Record.** Append the buyer pubkey to the retained element set; publish the new blob (A1 steps 3–4). 7. **Republish.** Publish the replacement `32217` with the new `x`, same `d`, later `created_at`. Delete the superseded filter blob with BUD-12 (`DELETE /` + BUD-11 `t=delete` token). 8. **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. 2. LNURL server returns a BOLT-11 invoice; buyer pays it. 3. LNURL server publishes the zap receipt (kind `9735`), which is signed by the LNURL server's key and embeds the original `9734`. 4. 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. 3. The retailer adds the buyer to its `retail:` filter and republishes it. 4. 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",""]`, `["period",""]`, and a token locked to `K_ret`. 2. 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::`. 3. The subscription filter MAY carry `["catalog","",...]` coverage tags. If it does not, the host resolves coverage from the owners' `retailer` tags plus the retailer's catalog. 4. While subscribed, the buyer streams covered assets — the host treats them as subscription serves (A10 step 4). 5. 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 │ │ │ 2. PUT /upload (new blob) │ │ │ 3. EVENT [32217] new x ──────────►│◄──── EVENT 32217 ─────│ │ DELETE / ──────────────►│ │ ``` - Removal is a full rebuild from the retained element set (bloom filters cannot delete). - The addressable replacement is the revocation signal; no separate revocation event exists. - **False-positive caveat:** a revoked pubkey may still match by false positive. At `fp = 1e-9` this is negligible; at the maximum permitted `fp = 1e-6` it is a known, accepted residual risk. - Expiry (`["expiration", …]`) is the time-boxed equivalent for subscription filters and rentals. ### A9 — Entitled re-download `[SPEC]` 1. Buyer builds a BUD-11 token: kind `24242`, `t = get`, `["x",""]`, `["expiration",""]`, `content` explaining the use; signs with `K_buy`; base64url. 2. `GET /` with `Authorization: Nostr ` (optionally `Range`). 3. Host runs the gating decision (A10). 4. 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 / [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: filter → serve, receipt (ownership) │ buyer ∈ R's sub:: filter │ and asset ∈ R's covered catalog → serve, receipt (subscription) │ └─ 5. otherwise → 402 + X-Cashu (NUT-24) ``` Details that implementations get wrong: - **Step 2 comes before 5.** A gated asset with no identity gets `401`, not `402`. The `402` is the challenge for a *known* buyer without entitlement (rental) or a bytes fee. - **Filters are fetched once and cached**, keyed by address (`32217::`). A new event at the address invalidates the cache. The host SHOULD verify the blob's sha256 against the event's `x` before trusting it. - **Filter verification order:** owner signature on the `32217` event → `x` matches the blob bytes → header magic/version/asset address match the event and the requested asset → parameters (`fp`, `n`, `k`) consistent with the bit-array length. - A filter whose asset address does not match the asset being served MUST be ignored. - Free assets are served by open `GET`, still with a receipt, so listen counts stay 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":[""],"#d":["fan:"]}] ``` 3. Verify the owner's signature on the event. 4. Fetch the blob from the `server` hint, falling back to the owner's NIP-B7 kind `10063` server list; verify `sha256(bytes) == x`. 5. Mirror the blob if desired (`PUT /mirror`, BUD-04) so gating survives the origin host. 6. 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 /` (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 / (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 / 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 length is host policy; the 402 request MUST state the amount and unit. - Settlement is at the end of the bucket; `amount_sats` is the settled amount. - No per-second metering, no escrow, no change. If the listener continues, buy the next bucket. - `HEAD` MUST NOT carry payment proof (BUD-07). ### 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`: ```json { "kind": 32218, "pubkey": "", "created_at": 1758130000, "content": "", "tags": [ ["a", "32210::"], ["p", ""], ["x", ""], ["server", "https://blossom.musiquay.com"], ["mode", "download"], ["start", "1758130000"], ["end", "1758130000"], ["bytes", "48216387"], ["amount_sats", "0"], ["paid", ""] ] } ``` 2. Seal (kind `13`) and wrap (kind `1059`) it **once to the buyer** and **once to the publisher** (two wraps, one rumor each). 3. Publish each wrap to the recipient's read relays. 4. The publisher archives every receipt for accounting. The buyer keeps theirs as proof of purchase. Rules: - One signature, the host's. There is no countersignature. - `mode` ∈ `download` | `stream`; `start`/`end` bound a stream session. - `amount_sats = 0` for entitled and free serves; the settled amount for rental/bytes. - `paid` lists token hashes spent at this serve; empty when access came from membership. - Receipts are **never** published to public relays and never queried by third parties. 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: ```json { "kind": 32219, "pubkey": "", "content": "", "tags": [ ["a", "32210::"], ["start", "1758130000"], ["end", "1760808400"], ["plays", "48213"], ["downloads", "3921"], ["bytes", "21474836480"], ["sats", "1510000"], ["listeners", "11054", "approx"] ] } ``` - Signed by the same host key that signed the underlying receipts, so counts are attributable. - Aggregates are immutable and regular: a correction is a new event with a revised period. - `listeners` is approximate and tagged as such — the filter's false-positive floor makes it an estimate. - Aggregates are the authoritative public totals. They are **not** derived by summing play reports. ### D3 — Publish a publisher rollup `[SPEC]` The publisher consolidates host aggregates by reference, preserving each host's signature: ```json { "kind": 32220, "pubkey": "", "content": "", "tags": [ ["a", "32210::"], ["start", "1758130000"], ["end", "1760808400"], ["e", "", "", ""], ["e", "", "", ""], ["receipts", "52134"], ["plays", "55123"], ["downloads", "4120"], ["bytes", "22817013760"], ["sats", "1730000"] ] } ``` - Each `e` tag is an authenticity chain link: clients verify every host aggregate independently, then the publisher's signed pointer-set. - `receipts` is the publisher's own archive count — deed-backed even when no host aggregate survives. - Rollups can be recomputed and re-published at any time. ### 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":[""]}] ``` (requires AUTH on relays that gate wraps). 2. Stores each unwrapped `32218` keyed by `(asset, host, period)`. 3. 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`: ```json { "kind": 3221, "pubkey": "", "content": "", "tags": [ ["a", "32210::", "wss://relay.musiquay.com"], ["p", ""], ["x", ""], ["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|||||| ``` 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": "", "s": "" } } # 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: ```json { "kind": 3221, "pubkey": "", "content": "", "tags": [ ["a", "32210::"], ["p", ""], ["x", ""], ["server", "https://blossom.musiquay.com"], ["codec", "mp3-128kbps-VBR"], ["t0", "1758130000"], ["t1", "1758130247"], ["blind", ""], ["host", ""], ["statement", ""], ["dleq", "", ""] ] } ``` Open problems, both requiring a decision before this flow is implementable: - **Verification is not `C == K·H(m)`.** On secp256k1 there is no pairing, so that check is 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). - **Blind signing is an oracle.** A host that signs `H(statement)` without seeing the 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 ──────────────────────│ ``` - Publish `31923` with `d`, `title`, `summary`, `image`, `start`, `end`, `D` (**required by 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). - RSVP is `31925` with `a` = event address, a unique `d`, `status` (`accepted`|`declined`|`tentative`), `fb` (`free`|`busy`), optional `p` = creator. - Discovery is by geohash prefix: query `#g` with the prefixes covering the radius, then 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",""]` (informational), `["joined_at",""]`, unique `d`. 2. **Observe.** Anyone may query the whole queue: ``` ["REQ","waitlist",{"kinds":[31925],"#a":[""]}] ``` Queue order is `joined_at` ascending, tie-broken by `d` ascending. `position` is a hint; clients recompute. 3. **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`). 4. **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:: 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: | Option | Mechanism | Identity-bound | Double-entry | Unlinkable | |--------|-----------|----------------|--------------|------------| | Cashu ticket (recommended) | P2PK token (NUT-11) locked to the buyer, issued from a seller swap | Yes — holder must sign a P2PK witness at the door | Yes — check-in burns the token at the mint | No | | Blind-signed ticket | BDHKE signature over an event/tier statement | No | No (a copy is indistinguishable) | Yes | | Wrapped deed | NIP-59 ticket with a venue-signed entitlement | Yes (DM key) | No | Partial | 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). 2. There is no `a` tag pointing at a separate roster — the address is the event address. 3. Order matters only for display; the roster has no positional semantics. ### S5 — Confirm, deny, correct `[SPEC]` A participant publishes NIP-32 kind `1985`: ```json { "kind": 1985, "pubkey": "", "content": "optional comment", "tags": [ ["a", "31923::", "wss://relay.musiquay.com"], ["p", ""], ["L", "musiquay.credit"], ["l", "confirmed", "musiquay.credit"], ["role", "sound-foh"] ] } ``` - `l` ∈ `confirmed` | `denied` | `corrected`; `corrected` carries the true role. - Kind `1985` is regular, so a person can publish several. `[NEW]` Client convention: for a given `(signer, target, role)` the **latest `created_at` wins** (ties broken by lowest id); earlier ones remain visible as history. - A denial never deletes the entry. Confirmation is signal, not a gate. ### S6 — Attest `[SPEC]` Same kind, different author and namespace: ```json ["a", "31923::"], ["p", ""], ["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: ```json { "kind": 1111, "pubkey": "", "content": "Adding the lighting tech.", "tags": [ ["A", "31923::", "wss://relay.musiquay.com"], ["K", "31923"], ["P", ""], ["a", "31923::", "wss://relay.musiquay.com"], ["k", "31923"], ["p", ""], ["p", "", "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`. ```json ["song", "5", "Song Title Five", "32210::", "feat", ""] ``` `[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::"]` 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. 3. Editions: `32216` with `d`, `a` = album address, format/edition tags, `i`/`k` external IDs, and edition-specific `p` roles. 4. 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. 3. **Order** — NIP-15 checkout is JSON in a **kind 4** direct message: `{"id","type":0,"items":[{"product_id","quantity"}],"shipping_id","contact":{…}}`. 4. **Payment request** — merchant replies `type: 1` with `payment_options` (`ln`, `lnurl`, `btc`, `url`). 5. **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`. 2. Drafts are `30403` with identical structure; publishing means emitting `30402`. 3. Contact is a DM. No escrow, no dispute mediation, no rating system: reputation is the scene graph (S9 + S4 history). 4. 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", ""] client: ["EVENT", ] relay: ["OK", "", false, "auth-required: only registered keys may write"] client: ["AUTH", ] relay: ["OK", "", true, ""] client: ["EVENT", ] # retry the same event relay: ["OK", "", true, ""] ``` - `22242` is ephemeral, never stored, never broadcast; `created_at` within ~10 minutes. - `restricted:` means authenticated but not authorized; `auth-required:` means not yet authenticated. Clients MUST NOT retry forever on `restricted:`. - AUTH is also what makes NIP-59 receipt delivery safe: relays SHOULD require it before serving `1059` events, and SHOULD serve them only to the `p`-tagged recipient. ### O2 — Replication and mirroring `[SPEC]` - **Relay ↔ relay:** public events are pushed to public relays so Musiquay content is 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 ↔ host:** blobs are content-addressed and mirrorable; `PUT /mirror` (BUD-04) lets a 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). - A publisher SHOULD publish to more than one relay and MAY publish to a paid persistence relay. Persistence is a service, not a protocol guarantee. ### O3 — Deletion and vanish `[SPEC]` - **Event deletion:** NIP-09 kind `5` with `e` (regular target) or `a` (addressable target) plus `k` (target kind). Best-effort: relays SHOULD honour it, replicated copies elsewhere may persist. - **Vanish:** NIP-62 request for a whole key's data. - **Blob deletion:** BUD-12 `DELETE /` with a BUD-11 `t = delete` token scoped by `server` and `x`. Unscoped delete tokens are dangerous — always scope them. - **Filter blobs:** superseded blobs SHOULD be deleted (A12); deleting a *current* filter 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. - Deletion does not revoke a purchase: an entitlement already granted lives in the filter until the owner rebuilds it (A8). Decide the policy per asset — withdrawal vs refund. ### O4 — Recovery and restore `[NEW]` | Lost thing | Recovery | |------------|----------| | Cashu wallet state | NUT-09 restore from the BIP-39 mnemonic; NUT-13 deterministic secrets mean proofs are re-derivable. | | Pending swap | NUT-07 `POST /v1/checkstate` for the input proofs; NUT-19 makes retrying a `mint`/`swap` with the same payload safe. | | Filter element set | Owner-local responsibility. Without the retained set, only the current filter blob is recoverable, and revocation/appends become impossible (A1). Owners SHOULD back it up. | | Receipts | Re-`REQ` the wraps: `{"kinds":[1059],"#p":[""]}` on each relay; the union of relays is the best available archive. | | Events generally | Re-`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). 4. Blob bytes hash to the `x` value they claim. 5. Payment proof: token decodes, unit matches, amount sufficient, proofs unspent, mint accepted. 6. Only then apply business logic. ### 11.2 Ordering and replacement - Addressable events: latest `created_at` per `(kind, pubkey, d)`; ties broken by lowest id. - Regular events: immutable. Corrections are new events. - Receipts and play reports are never replaced. Aggregates are never replaced (revised periods are new events). - A host's cache of a filter is invalidated by **the event at the address changing**, not by the `x` tag changing in isolation. An event whose `x` does not match its blob is invalid. ### 11.3 Idempotency | Operation | Idempotent | Notes | |-----------|------------|-------| | `PUT /upload` | Yes | `200` vs `201` distinguishes existing from new; bytes are content-addressed. | | Bloom insert | Yes | Setting bits twice is a no-op. | | Filter republish | Yes | Same element set ⇒ same blob ⇒ same `x`. | | `POST /v1/swap` | Only with NUT-19 | Same payload may be served from cache; do not build new outputs per retry. | | Receipt issuance | Yes | Re-serving the same buyer for the same blob produces a new deed; that is correct, not a duplicate. | | Play report publish | Yes | Duplicate reports from one key inflate counts; clients SHOULD NOT re-publish on retry. | ### 11.4 Time - `created_at` is author-controlled and untrusted; `expiration` (NIP-40) is advisory to relays and authoritative only where a flow says so (subscription filters, tokens, BUD-11 auth tokens). - A host SHOULD allow modest clock skew and MUST treat an expired BUD-11 token as invalid. - Receipts' `start`/`end` are host wall-clock and are the settled window. ### 11.5 Privacy - Receipts never leave NIP-59 wraps. A relay operator who can link a `1059` to a recipient breaks the privacy of the deed; hence AUTH-gated wrap delivery. - The `paid` field carries token **hashes**, never tokens. - Blind-sign requests MUST NOT be retained by the host (§8 R2). - Aggregate `listeners` is approximate by construction and MUST be marked so. - Filter element sets are published in the clear. Membership in a fan filter is public information at the bit level; do not describe it as private. ### 11.6 Failure modes | Signal | Meaning | Correct reaction | |--------|---------|------------------| | `["OK", id, false, "duplicate: …"]` | Relay already has it | Treat as success. | | `["OK", id, false, "invalid: …"]` | Malformed/verification failure | Fix, do not blind-retry. | | `["OK", id, false, "rate-limited: …"]` | Back off | Retry with delay. | | `["CLOSED", sub, "auth-required: …"]` | Need NIP-42 | Run O1, then re-`REQ`. | | `400` + `X-Reason` | Bad/expired/insufficient payment proof | Rebuild the proof; do not retry as-is. | | `401` from Blossom | No/invalid BUD-11 token | Sign a fresh `24242` token and retry. | | `402` from Blossom | Payment required | Pay the `X-Cashu`/`X-Lightning` challenge, retry the `GET`/`PUT` (never `HEAD`). | | `404` | Blob or filter absent | Fall back to the `server` hint, then the owner's `10063` list. | | `409` from Blossom | `X-SHA-256` mismatch | Bug; do not retry. | | swap rejected by mint | Spent/invalid proofs | The payment did not happen; no entitlement. | | no wrap arrives | Delivery failure | Re-`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](./ARCHITECTURE.md) registry and [FEATURES_ACCESS.md](./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](./FEATURES_MUSIC.md)), so `{"kinds":[32210],"#x":[""]}` 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](./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||||`), and the pair-check claim is corrected in both [FEATURES_ACCESS.md](./FEATURES_ACCESS.md) and [FEATURES_MUSIC.md](./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](./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](./FEATURES_CALENDAR.md) now records the `["capacity",""]` 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](./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](./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](./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](./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](./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** - [ ] NIP-01 `EVENT`/`REQ`/`CLOSE`/`OK`/`EOSE`/`CLOSED`/`NOTICE` with correct prefixes. - [ ] NIP-45 `COUNT` over `3221` play reports. - [ ] NIP-42 AUTH, including `auth-required:` on `REQ` and `EVENT`. - [ ] Addressable semantics for `32210`–`32217`; regular storage for `3221`, `32218`, `32219`, `32220`. - [ ] Kind allow-list includes the Musiquay block. - [ ] `1059` serving gated to the `p`-tagged recipient, ideally behind AUTH. **Host** - [ ] BUD-01/02/04/06/11/12 endpoints and status codes. - [ ] BUD-11 token validation: kind, `created_at`, `expiration`, `t`, `server`, `x`. - [ ] BUD-07 `402` with a valid NUT-24 `X-Cashu` challenge; payment-proof retry only on `GET`/`PUT`. - [ ] Blob → asset resolution (G-1) and filter lookup by address. - [ ] Filter verification: event signature, `x` match, header match, parameter consistency. - [ ] Gating order A10 including `401` before `402`. - [ ] Receipt issuance to buyer and publisher (D1), archive, aggregate (D2). - [ ] Rental window accounting and `mode = stream` receipts (A13). **Publisher / Owner** - [ ] Retains the filter element set; can append and rebuild (A1/A8). - [ ] Claims payments correctly — swap decoded and unblinded (G-7). - [ ] Publishes and re-publishes rollups referencing host aggregates (D3). - [ ] Archives receipts (D4). **Client** - [ ] Address-based references everywhere; never event ids for addressable kinds. - [ ] BUD-11 auth for gated fetches; NUT-24 challenge decoding (needs NUT-18/26). - [ ] Cashu wallet with P2PK send/receive, NUT-07 state, NUT-09 restore. - [ ] NIP-59 wrap/unwrap for receipts (or the G-5 alternative). - [ ] Latest-wins resolution for addressable events and confirmations. - [ ] Never publishes `32218` to a public relay; never publishes `30315` as a play. ### 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. 6. Host publishes a `32219` aggregate; owner publishes a `32220` rollup. 7. Buyer publishes a `3221` play report; relay `COUNT` includes it. 8. 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.