{"id":"49da6a793f9d94d63054389c3a480f1192e7412a613766fb4277fe656a5d5b01","pubkey":"f7922a0adb3fa4dda5eecaa62f6f7ee6159f7f55e08036686c68e08382c34788","created_at":1786660673,"kind":30817,"tags":[["d","pc-20-favorites"],["title","PC 2.0 Favorites"],["alt","Nostr Implementation Possibility: PC 2.0 Favorites"],["client","NostrHub"]],"content":"# Cross-app Podcast Favorites on Nostr\n\nFavorite a show or a track in one Podcasting 2.0 app, and it's favorited in every other app you sign into. One flat list, one event, no merge algorithm.\n\n## How it works\n\nKind **`10333`** — a plain (non-`d`-tagged) replaceable event, so there is exactly one per pubkey. Republishing the full tag list replaces the previous version wholesale; that replacement *is* the sync mechanism.\n\nEntries are [NIP-73](https://github.com/nostr-protocol/nips/blob/master/73.md) identifiers, one `i` tag each, grouped by medium so it isn't repeated per feed. `k` tags name the identifier *kinds* the event contains — one per distinct kind, at the end, not one per entry.\n\n```json\n{\n  \"kind\": 10333,\n  \"tags\": [\n    [\"alt\", \"PC 2.0 Favorites\"],\n\n    [\"medium\", \"podcast\"],\n    [\"i\", \"podcast:guid:<feedGuid>\"],\n    [\"i\", \"podcast:item:guid:<itemGuid>\"],\n\n    [\"medium\", \"music\"],\n    [\"i\", \"podcast:guid:<feedGuid>\"],\n    [\"i\", \"podcast:item:guid:<itemGuid>\"],\n\n    [\"k\", \"podcast:guid\"],\n    [\"k\", \"podcast:item:guid\"]\n  ],\n  \"content\": \"\"\n}\n```\n\n`medium` is a running value: it applies to every entry after it until the next `medium` tag. A `podcast:guid` entry opens a feed group; the `podcast:item:guid` entries under it belong to that group until the next feed or medium tag.\n\n## Tag order is load-bearing\n\nAn item's parent feed and its medium are both carried by *position*, not by anything on the entry itself. A client that parses entries into structs and rebuilds the tag array — sorting, deduplicating, reordering — silently reattaches every item to the wrong feed, and nothing else in the format recovers the association. Preserve the order you read, and append rather than rebuild.\n\n## Three sharp edges\n\n- **A feed group is not always a favorite.** Opening a group is the only way to say which feed an item came from, so a group appears whether or not the user favorited the feed. On the first real list published in this format, 196 groups carried only 82 favorited feeds — the other 114 existed solely so a favorited track could name its parent. Treat an *itemless* group as a real favorite and a group with items as unknowable; inventing a favorite is worse than missing one.\n- **`medium` is a hint, never truth.** It exists so a client can bucket \"Podcast\" vs. \"Music\" without an API round trip. A Podcast Index lookup on the guid wins whenever they disagree, and an entry sitting before any `medium` tag is *unknown* — not `podcast`.\n- **One bad relay read, republished, is the whole list gone.** A relay answering \"I have nothing\" and a relay that never answered must produce different results. An aggregate EOSE from your relay library is not by itself proof the read succeeded.\n\n## What it doesn't do\n\nNo merge algorithm — two apps writing concurrently clobber each other, which is what the single-writer assumption exists to satisfy. No split between shows and items, so the whole list is one event: a real one runs 36 KB for 196 feeds and 227 items, against a ~128 KB relay cap, and item favorites accumulate an order of magnitude faster than feed favorites.\n\nKind `10333` is **self-assigned, not NIP-allocated** — unclaimed in the kind registry as of this writing, but confirm there's still no collision before depending on it. Relay filters are kind-scoped, so a later NIP landing on 10333 would put two unrelated event types into every query.\n\nSpec, test vectors, and open questions: https://github.com/ChadFarrow/PC20-Nostr","sig":"952fb94cc13e98598045fca1d1ec4f173bb9863f92f3232cb1c6e821f462b43cc56eb6f1114a698591d98cf85aa2d25434964283d3614a7e6e433a2b39454cfe"}