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.
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:
| 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 and FEATURES_ROSTER.md.
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.
Client → relay:
| Message | Form | Use |
|---|---|---|
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:
| Message | Form | Use |
|---|---|---|
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:
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.
ids, authors, #e, #p MUST be exact 64-character lowercase hex.{"#x": [...]} works; {"#audio": [...]}does not. This constrains asset/blob lookup — see §12 G-1.
REQ are OR'd; all conditions inside one filter are AND'd.(kind, pubkey, d); ties on created_at are broken by lowest event id.
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 /<sha256>[.ext] | 01 | Fetch a blob. Supports Range (206). |
HEAD /<sha256>[.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 /<sha256> | 12 | Delete a blob. |
GET /list/<pubkey> | 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 <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
HEAD responses to a 402 MUST NOT be retried with payment proof; the client pays and retries the GET/PUT.
X-Cashu: cashuB... (or X-Lightning: <preimage>) and no other change.
400 + X-Reason.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/<quote_id> | 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:
cashuA (JSON+base64), V4 cashuB (CBOR+base64url). V4 tokens may carry a memo m. New flows use V4.
s, Y = hash_to_curve(s), blinding scalar r, 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.
creqA / creqB encodings. NUT-24 requires both.mint/swap with the same payload.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", <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).
| Object | Address or id | Notes |
|---|---|---|
| Track | a: 32210:<publisher>:<d> | d SHOULD be the ISRC when one exists. |
| Album | a: 32211:<publisher>:<d> | d SHOULD be the MusicBrainz release-group MBID. |
| Playlist | a: 32212:<author>:<d> | NIP-51 set conventions. |
| Setlist | a: 32213:<author>:<d> | References the roster address. |
| Release/track credits | a: 32214:…, a: 32215:… | Track credits reference both a track and the release credits. |
| Edition | a: 32216:<publisher>:<d> | References the album address. |
| Access filter | a: 32217:<owner>:<d> where d is <tier>:<asset-address> | tier ∈ fan | retail | sub. |
| Play report | e: <id> | Regular; d-less; counted with NIP-45. |
| Delivery receipt | e: <id> | Never public; exists only inside wraps. |
| Host aggregate | e: <id> | Regular, public. |
| Publisher rollup | e: <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).
| # | 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] |
[SPEC]Precondition: the client holds the bytes and a keypair K_usr.
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.
PUT /upload with the binary body, Content-Type, Content-Length, X-SHA-256, and the same Authorization header.
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.
[SPEC](sha256, codec-spec, server-url, media-type, size).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.
["EVENT", <track>] to each write relay.["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 eachaudiotag. See §12 G-1.
[SPEC]audio tags plus the new one, same d, fresh created_at.
the new event; clients that cached the old event id MUST NOT keep referencing it.
[SPEC]32211 with d, metadata, price if the release as a whole is priced, and the ordered a tags. Tag order is the tracklist.
32216) and credits (32214) reference the albumaddress afterwards.
[SPEC]32212 with d, title, description, image, and ordered a tags..content (NIP-44, self-key), with no member a tags.
["EVENT", <playlist>].[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:
#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.
EOSE-close each bootstrap subscription (["CLOSE","boot-…"]) once it haswhat it needs; a catalog subscription left open is a live feed.
reuse the bootstrap ids.
[SPEC]The owner publishes the price on the asset, and a filter event per tier.
["price","<sats>","sat"]. Absent ⇒ free.[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).
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).
PUT /upload the blob (P1). Record the returned sha256.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"]
]
}
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.
[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.
[SPEC]The retailer authors, for each asset it sells:
d = retail:<asset-address>, element set = buyers who tookownership through it;
d = sub:<retailer-pubkey>:<channel-d>, element set = active subscribers, optionally with ["catalog", "<asset-address>", ...] coverage tags.
Both are kind 32217 with the same blob mechanics as A1, authored by the retailer.
[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) │ │
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.
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).
["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>"].
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).
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)
(A1 steps 3–4).
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).
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)
9734) for the owner, with amount = price and the asset a tag; sends it to the owner's lud16 LNURL callback.
9735), which is signed by the LNURL server's key and embeds the original 9734.
9735 events addressed to K_own, verifies the embeddedrequest 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.
[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.
tier = retail.On-behalf model — the retailer collects on the owner's behalf.
tier = retail.itself), i.e. it forwards the exact amount.
retail:<asset-address> filter and republishes it.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.
[NEW] 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.
the currently active subscribers (including this buyer) and republishes
d = sub:<retailer>:<channel-d>.
["catalog","<asset-address>",...] coverage tags. If it does not, the host resolves coverage from the owners' retailer tags plus the
retailer's catalog.
serves (A10 step 4).
filter is the membership state.
[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.
[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.
["expiration", …]) is the time-boxed equivalent for subscription filters andrentals.
[SPEC]24242, t = get, ["x","<blob-sha256>"], ["expiration","<now+300>"], content explaining the use; signs with K_buy; base64url.
GET /<sha256> with Authorization: Nostr <token> (optionally Range).200, the host issues a receipt (D1) asynchronously. Re-downloads are unlimited foras long as membership holds; no token or payment is presented on the request itself.
[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:
401, not 402. The 402 is the challenge for a known buyer without entitlement (rental) or a bytes fee.
32217:<author>:<d>). 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.
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.
GET, still with a receipt, so listen counts staycertifiable.
[SPEC]A host that agrees to the terms serves without contacting the owner:
` ["REQ","onboard",{"kinds":[32217],"authors":["<owner>"],"#d":["fan:<asset-address>"]}]
`
server hint, falling back to the owner's NIP-B7 kind 10063 server list; verify sha256(bytes) == x.
PUT /mirror, BUD-04) so gating survives the origin host.[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.
[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)
amount_sats is the settled amount.bucket.
HEAD MUST NOT carry payment proof (BUD-07).[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.
[SPEC]Triggered by every serve — free, owned, subscription, rental, bytes-fee.
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>"]
]
}
13) and wrap (kind 1059) it once to the buyer and **once to thepublisher** (two wraps, one rumor each).
purchase.
Rules:
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. Relays SHOULD only serve 1059 to the p-tagged recipient and SHOULD require AUTH.
[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.
listeners is approximate and tagged as such — the filter's false-positive floor makes itan estimate.
reports.
[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"]
]
}
e tag is an authenticity chain link: clients verify every host aggregateindependently, then the publisher's signed pointer-set.
receipts is the publisher's own archive count — deed-backed even when no host aggregatesurvives.
[NEW]The publisher's archive is the durable record. A conforming publisher:
`
["REQ","receipts",{"kinds":[1059],"#p":["<publisher>"]}]
`
(requires AUTH on relays that gate wraps).
32218 keyed by (asset, host, period).32220 rollups whenever aggregates arrive or the archivechanges. Signatures on the referenced aggregates survive re-publication.
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.
[SPEC]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.
[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).
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.
[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.
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.
[SPEC]creator relay attendee
│ EVENT [31923] ─────────────────► │
│ ◄── OK │
│ │ ◄──── REQ {"kinds":[31923],"#g":["eyckch"]} ────│
│ │ ───── EVENT 31923 … EOSE ──────────────────────►│
│ │ ◄──── EVENT [31925] RSVP ──────────────────────│
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).
31925 with a = event address, a unique d, status (accepted|declined|tentative), fb (free|busy), optional p = creator.
#g with the prefixes covering the radius, then filter precisely client-side. g is single-letter and therefore indexed.
[OPEN]31925 with ["status","waitlist"], ["position","<n>"] (informational), ["joined_at","<unix>"], unique d.
`
["REQ","waitlist",{"kinds":[31925],"#a":["<event-address>"]}]
`
Queue order is joined_at ascending, tie-broken by d ascending. position is a hint;
clients recompute.
the attendee replaces their own RSVP (status = accepted, same d).
social action, not a protocol transition. See G-11.
[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:
| 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.
[SPEC]The post-show roster is the calendar event, replaced.
31923 at the same d, keeping all calendar fields, with the complete p roster (p = pubkey, relay hint, role from the roster vocabulary).
a tag pointing at a separate roster — the address is the event address.[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"]
]
}
l ∈ confirmed | denied | corrected; corrected carries the true role.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.
[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.
[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.
[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.
[SPEC]32214 with d, a = album address, and p-with-role tags.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.
32216 with d, a = album address, format/edition tags, i/k external IDs, and edition-specific p roles.
the same namespaces.
[SPEC]30017, content = NIP-15 JSON (id, name, currency, shipping).30018, content = NIP-15 JSON (id, stall_id, name, price, quantity,specs, shipping extras). Physical music links its edition via the product specs.
{"id","type":0,"items":[{"product_id","quantity"}],"shipping_id","contact":{…}}.
type: 1 with payment_options (ln, lnurl, btc, url).
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.
[SPEC]30402 with d, title, summary, published_at, price (["price","500","EUR","day"] for frequency-priced services), location, g, t,
image, status.
30403 with identical structure; publishing means emitting 30402.scene graph (S9 + S4 history).
listing.
[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, ""]
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:.
serving 1059 events, and SHOULD serve them only to the p-tagged recipient.
[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.
PUT /mirror (BUD-04) lets ahost 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.
[SPEC]5 with e (regular target) or a (addressable target) plus k (target kind). Best-effort: relays SHOULD honour it, replicated copies elsewhere
may persist.
DELETE /<sha256> with a BUD-11 t = delete token scoped by 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.
[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":["<publisher>"]} 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. |
Every consumer verifies in this order, and stops on the first failure:
id = sha256 of the NIP-01 serialisation; sig valid for pubkey.a tag) parses as kind:pubkey:d, and the kind is the expected one.aggregates, publisher signs rollups, the participant signs their own confirmation).
x value they claim.accepted.
created_at per (kind, pubkey, d); ties broken by lowest id.periods are new events).
the x tag changing in isolation. An event whose x does not match its blob is invalid.
| 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. |
created_at is author-controlled and untrusted; expiration (NIP-40) is advisory torelays and authoritative only where a flow says so (subscription filters, tokens, BUD-11 auth tokens).
start/end are host wall-clock and are the settled window.1059 to a recipientbreaks the privacy of the deed; hence AUTH-gated wrap delivery.
paid field carries token hashes, never tokens.listeners is approximate by construction and MUST be marked so.information at the bit level; do not describe it as private.
| 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. |
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.
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".
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.
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
EVENT/REQ/CLOSE/OK/EOSE/CLOSED/NOTICE with correct prefixes.COUNT over 3221 play reports.auth-required: on REQ and EVENT.32210–32217; regular storage for 3221, 32218, 32219, 32220.1059 serving gated to the p-tagged recipient, ideally behind AUTH.Host
created_at, expiration, t, server, x.402 with a valid NUT-24 X-Cashu challenge; payment-proof retry only on GET/PUT.x match, header match, parameter consistency.401 before 402.mode = stream receipts (A13).Publisher / Owner
Client
32218 to a public relay; never publishes 30315 as a play.One scripted sequence that exercises the whole core, and nothing else:
price tag and an empty fan filter (n = 0).GET the blob unauthenticated → 401; with a valid BUD-11 token → 402.32217, deletes the old blob.200; host issuesreceipts to buyer and owner.
32219 aggregate; owner publishes a 32220 rollup.3221 play report; relay COUNT includes it.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.