Access is a signed filter, not a receipt per buyer. Payment buys membership; the filter gates the bytes; deeds record what was served.
Musiquay's paid-access layer answers three separate questions, and keeps them separate:
The design deliberately avoids per-buyer entitlement events. One addressable filter per asset (per tier) holds every authorized pubkey, backed by a Blossom blob so the filter can grow to any size while the event stays tiny. Payment proves a purchase once; membership in the filter grants unlimited re-downloads and streaming, like owning a record rather than renting a stream.
This is the Bandcamp model - pay once, re-download forever, every encoding - rebuilt on Nostr, Blossom, and Cashu, with the trust made explicit instead of entrusted to a platform.
A paid asset is gated by a probabilistic membership filter (Bloom) containing the pubkeys of everyone entitled to it. The filter is signed by the asset owner and published as an addressable event. Any host that agrees to the terms can fetch the filter, verify the signature, and gate downloads without contacting the owner.
1e-9, grinding keypairs until one lands in the filter costs far more than the asset. Owners MAY choose a higher rate for very large filters.n and byte size are public - a digital gold record. Owners SHOULD publish accurate n.An asset is free if and only if no access filter exists for it. Free assets are served by open GET, with no entitlement check. Hosts still issue delivery receipts so listen counts remain certifiable. A priced asset (price tag present) MUST have a filter; if it does not, hosts treat it as free - the filter, not the price tag, is what gates.
A delivery receipt is a private, host-signed document recording one serve: which asset, which blob, which pubkey, how many bytes, over what time window, for what payment. It is a deed - one signature from the serving host - not a contract, so there is no countersignature. Receipts are encrypted to the buyer and to the publisher, and delivered only over authenticated channels. They never appear in public feeds.
Two payment modes coexist:
A host MAY separately charge for bytes served (its own hosting policy); that is orthogonal to entitlement and lives outside this spec.
A retailer does not hold the asset and does not take a cut of the owner's fee. A retailer sells access to its channel - storefront, curation, discovery, community, and the delivery rails it operates. Two independent payments exist:
Nothing is free to run: someone pays for storage and delivery, whether through a subscription (a "paywall" in the relay sense), a per-asset fee, or a wholesale arrangement. The protocol does not mandate any of these, but it does not pretend the rails cost nothing.
A retailer's subscription is a time-boxed membership set, represented with the same filter machinery: kind 32217, d = "sub:<retailer-pubkey>:<channel-d>", authored by the retailer, containing the pubkeys of currently subscribed members. The retailer republishes it each period (or on renewal/lapse), so membership in the latest version is the current subscriber set. No per-subscriber event is needed, and lapse is simply removal at the next rebuild.
A subscription filter MUST carry a catalog tag - a list of covered asset addresses - or the retailer MUST publish an addressable catalog list the filter's d resolves to. The retailer's catalog is authoritative for what a subscription covers; the owner's retailer tag on a fan filter is authorization to sell, not coverage. One source of truth avoids a host disagreeing with itself about what a subscriber may stream.
The filter event carries parameters and a pointer to a Blossom blob containing the filter itself.
{
"kind": 32217,
"pubkey": "<asset-owner-pubkey>",
"content": "",
"tags": [
["d", "fan:32210:<publisher-pubkey>:<track-d-tag>"],
["filter", "bloom"],
["fp", "1e-9"],
["n", "1234"],
["k", "30"],
["x", "<filter-blob-sha256>"],
["server", "https://blossom.musiquay.com"],
["mint", "https://mint.example.com"],
["retailer", "<retailer-pubkey>"],
["expiration", "1799999999"]
]
}
Design notes:
32217:<owner-pubkey>:<tier>:<asset-address>. A host needs only the asset address and the owner pubkey to find and verify the filter.fp no higher than 1e-6 and requires k = round(-log2(fp)).["mint", "lightning"] if the owner accepts direct Lightning payment. The filter event is the entitlement contract, so it is the right place to publish the payment legs.x. Revocation is the same operation with the pubkey removed.Filter blob format: a short header followed by the bit array, so the hash binds everything:
magic "MQF1" | version | asset address | fp | n | k | bit array
The blob lives on Blossom and mirrors like any other blob (BUD-04). Hosts SHOULD delete the superseded filter blob (BUD-12 DELETE) once the replacement event is observed; superseded filters have no archival value.
| Tier | d tag | Authored by | Contains |
|---|---|---|---|
| Fan | fan:<asset-address> | Asset owner | Direct buyers |
| Retail | retail:<asset-address> | Retailer | The retailer's per-asset buyers |
| Sub | sub:<retailer-pubkey>:<channel-d> | Retailer | Active channel subscribers |
The owner's fan filter carries retailer tags. There is no separate retailer filter authored by the owner - the retailer authors their own per-asset and subscription filters.
The owner declares a minimum fee on the asset event itself:
["price", "500", "sat"]
This fee is the owner's revenue and buys ownership. It is paid to the owner directly, whether the buyer arrives through the owner or through a retailer. The retailer's revenue is separate (a subscription), so no margin is derived and no protocol field is needed for one.
A purchase is a message, not an implicit side effect of a payment. The buyer seals an order rumor with NIP-59 and wraps it to the seller; the seller's sales service unwraps it, validates the payment, and adds the buyer to the filter.
{
"kind": 3222,
"pubkey": "<buyer-pubkey>",
"created_at": 1758130000,
"content": "",
"tags": [
["a", "32210:<owner-pubkey>:<track-d-tag>", "wss://relay.musiquay.com"],
["p", "<seller-pubkey>"],
["tier", "fan"],
["price", "500", "sat"],
["payment", "cashu"],
["mint", "https://mint.example.com"],
["token", "cashuB..."]
]
}
Design notes:
tier is fan (ownership direct), retail (ownership through a retailer), or sub (channel subscription). Subscription orders add ["channel", "<channel-d>"] and ["period", "<seconds>"].payment is lightning and the proof is the NIP-57 zap receipt (kind 9735) that the LNURL server publishes after the buyer pays a zap request (kind 9734) carrying the asset a tag. The buyer's pubkey comes from the zap request, so entitlement still follows from the payment.A receipt is an inner document carried in a NIP-59 gift wrap: the wrap hides sender and recipient from relays, the inner seal is signed by the serving host, and the payload is encrypted to the recipient. One wrap is sent to the buyer, one to the publisher.
Inner receipt payload:
{
"kind": 32218,
"asset": "32210:<publisher-pubkey>:<track-d-tag>",
"buyer": "<buyer-pubkey>",
"blob": "<blob-sha256>",
"server": "https://blossom.musiquay.com",
"mode": "download",
"start": "1758130000",
"end": "1758130000",
"bytes": "48216387",
"amount_sats": "0",
"paid": []
}
Design notes:
download or stream. For stream, start/end bound the session and amount_sats is the settled amount.0 for an entitled download (the purchase already happened) and for free assets; for a paid rental it is the settled amount.Play reports remain the listener-side public signal, separate from private receipts:
(statement, C, host-pubkey, dleq). Verification is by the NUT-12 DLEQ proof, not by a pairing equation: C == K * H(statement) is not computable on secp256k1. The blind signature keeps the listener unlinkable to the session; the DLEQ proof keeps the play certified. Full signing and verification detail is in FEATURES_MUSIC.md and PROTOCOL_FLOWS.md.Hosts publish signed totals per asset and period. The signature is the same host key that signed the underlying receipts, so counts are attributable.
{
"kind": 32219,
"pubkey": "<serving-host-pubkey>",
"content": "",
"tags": [
["a", "32210:<publisher-pubkey>:<track-d-tag>"],
["start", "1758130000"],
["end", "1760808400"],
["plays", "48213"],
["downloads", "3921"],
["bytes", "21474836480"],
["sats", "1510000"],
["listeners", "11054", "approx"]
]
}
listeners comes from the host's served-filter and is marked approx - the filter's false-positive floor makes it an estimate, and the tag says so.The publisher consolidates host aggregates without losing authenticity by referencing them rather than restating them. The rollup is publisher-signed; each leaf remains host-signed.
{
"kind": 32220,
"pubkey": "<publisher-pubkey>",
"content": "",
"tags": [
["a", "32210:<publisher-pubkey>:<track-d-tag>"],
["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 tags are the authenticity chain: clients verify each host aggregate independently, then the publisher's rollup as a signed pointer-set plus the publisher's own receipt-archive totals (receipts).["price", "500", "sat"] on the asset, paid to the owner. This is the content revenue and it is never shared with the retailer.["retailer", "<pk>"] tags for each authorized retailer.d = "retail:<asset-address>") for buyers who took ownership through them, and a subscription filter (d = "sub:<retailer-pubkey>:<channel-d>") for active subscribers.if buyer in owner fan filter (d = fan:<asset>) -> entitled (ownership, direct)
else for each ["retailer", R] on the owner filter:
if buyer in R's per-asset filter (d = retail:<asset>) -> entitled (ownership, via R)
if buyer in R's subscription filter
and asset is in R's covered catalog -> entitled (subscription access)
else -> 402 / not entitled
Single-signer per level: the owner signs the fan filter and the retailer list; each retailer signs their own filters. The owner can go offline after onboarding a retailer.
Ownership purchase (forward model):
tier = retail.Ownership purchase (on-behalf model):
tier = retail.d = retail:<asset-address>.The on-behalf model is a trust relationship: the retailer holds a token locked to the owner and is trusted to forward it. The protocol cannot enforce forwarding. Where that matters, use the forward model or the owner's own checkout.
Subscription:
tier = sub, the channel, and the period.Direct sales are the same ownership flow without the retailer: the owner's service adds the buyer to the fan filter. Subscription access is always a retailer-provided channel service; the owner's fee is always the ownership path.
One payment of the owner's fee. The buyer is added to the fan filter. Downloads and streams are then free of per-request payment. Re-downloads are unlimited and every encoding on the track is covered - one entitlement, all formats, exactly like Bandcamp.
A recurring payment to a retailer for its channel. The retailer maintains a subscription filter and the buyer streams the covered catalog while subscribed. This is the "paywall" in its natural sense: a subscription to a service, not a toll on each asset. It funds the delivery rails and the retailer's curation.
A host that is not running a subscription channel may still sell time directly:
402 with a NUT-24 X-Cashu payment request for one bucket (bucket length is host policy, e.g. 10 minutes).start/end/amount_sats.No escrow, no per-second micropayments, no change handling. The window is a bucket; the receipt is the deed.
A host MAY charge for bytes served independently of entitlement - its hosting economics. That charge is a host policy expressed through the same NUT-24 mechanism; it does not affect filter membership.
A buyer may always pay more than the fee, and a listener may tip a free asset. A tip is a payment leg, not a second entitlement mechanism:
price is a tip to the owner and is carried in the same purchase order and token.amount_sats with paid listing the token hashes. The voluntary-payment path in the prototype maps onto this rule.Paid play reports reuse Cashu's NUT-00 BDHKE blind signature scheme on secp256k1. The host holds a signing key k, public K = k*G. The listener holds a statement m and blinds Y = hash_to_curve(m) with a secret scalar r to produce B_ = Y + r*G; the host returns the blinded signature C_ = k*B_ without seeing m; the listener unblinds to C = C_ - r*K = k*Y.
Verification requires a DLEQ proof. The check C == K*H(m) is not computable on secp256k1 - it is a pairing equation, and secp256k1 has no pairing. A third party can only verify a blind signature from the NUT-12 DLEQ proof (e, s) that accompanies it, which proves log_G(K) == log_Y(C) without revealing k. Listeners therefore publish the proof alongside the statement, and play reports carry a dleq tag (see FEATURES_MUSIC.md). Without it, a play report is unverifiable by anyone but the host.
Blind signing is an oracle. A host that signs H(statement) without seeing the statement can be made to certify false statements, so the statement is restricted to facts the host itself observed - asset, blob hash, and time window. Money is accounted by receipts and aggregates, not by play reports, so tier and amount are deliberately not part of the signed statement.
Properties that matter:
0. Resolve the requested blob to an asset address
(sha256 -> asset, from the host's ingest index or a #x query)
1. No filter event for the asset (free asset) -> serve
2. No requester identity on a gated asset -> 401 + BUD-11 challenge
3. Buyer pubkey in owner fan filter -> serve, receipt (ownership)
4. Buyer pubkey in any authorized retailer's per-asset filter -> serve, receipt (ownership)
5. Buyer pubkey in a retailer's subscription filter
and asset is in that retailer's covered catalog -> serve, receipt (subscription)
6. Otherwise -> 402 + NUT-24 payment request
t = get and an x tag for the blob, sent as Authorization: Nostr <base64url>. A gated asset with no identity is a 401, not a 402; 402 is the challenge for a known buyer who is not entitled (rental) or who owes a bytes fee. NIP-42 is relay authentication and has no part in Blossom requests.x tag before trusting it.filter value or format version) MUST refuse to serve rather than serve ungated. An asset counts as free only when no filter event exists at all.price needs a rule. The proposed model is an album filter (d = fan:<album-address>) that the host consults after resolving blob -> track -> album, when the track itself has no filter. This is recorded as an open decision in PROTOCOL_FLOWS.md; until it is settled, hosts treat album price tags as informational.A host prioritizes buyers who have not yet downloaded an asset: first acquisition is more valuable to the network than a repeat fetch. To make that decision cheaply, the host keeps a local served-filter - a low-false-positive Bloom filter of (asset, buyer) pairs already served. This filter is host-local, never published, and its false positives are benign (a repeat downloader occasionally gets priority). No new event kind is involved.
Any host that accepts the filter terms can serve the asset: fetch the addressable filter event, verify the owner's signature, gate by membership. This is how hosting scales without a platform - the filter is the shared contract, and the bytes are content-addressed and mirrorable.
When a host observes a new version of a filter event, it SHOULD delete the superseded blob (BUD-12) to keep Blossom storage bounded. Old filters have no archival value once replaced.
| Concept | NIP Status | Notes |
|---|---|---|
| Access filter (32217) | Custom kind | No standard defines a signed probabilistic access set; NIP-63 kind 1163 (unmerged) is the nearest structural precedent. |
| Delivery receipt (32218) | Custom kind, NIP-59 delivery | Private deed; carried in gift wraps, one signature from the host. |
| Host aggregate (32219) | Custom kind | Public, host-signed totals. |
| Publisher rollup (32220) | Custom kind | Public, publisher-signed, references host aggregates by e tag. |
| Purchase order (3222) | Custom kind, NIP-59 delivery | Private buyer-to-seller order carrying intent plus payment proof. Never published. |
| Play report (3221) | Custom kind | Listener-side; BDHKE blind signature for paid plays. |
| Blind signatures | Cashu NUT-00 BDHKE + NUT-12 DLEQ | Reused directly; the DLEQ proof is what makes a play report verifiable by third parties. |
| Payment requests | NUT-24 / BUD-07 | HTTP 402 + X-Cashu; buckets for rental. Requires NUT-18/26 creqA/creqB client support. |
| Media authorization | BUD-11 / kind 24242 | How a host learns a requester's pubkey for a Blossom fetch. Not NIP-42. |
| Lightning payment leg | NIP-57 | Zap request/receipt for ownership paid in sats. |
| Blob storage and mirroring | NIP-B7 / BUD-01/04/12 | Filter blobs and audio blobs. |
| Private delivery | NIP-59 | Gift wrap for receipts. |
| Addressable events | NIP-01 | Filters are parameterized replaceable, d tag. |
| Addressing | NIP-19 | naddr for filters and assets. |