clone

every nostr nip as a chapter of an illustrated, early-2000s printed game manual

main
nip.guide / site / nips / f4.html
f4.html 22.0K
1<!DOCTYPE html>2<html lang="en">3<head>4<meta charset="utf-8">5<meta name="viewport" content="width=device-width, initial-scale=1">6<title>NIP-F4 - Podcasts — Nostr Instruction Manual</title>7<meta name="description" content="NIP-F4: podcasts on Nostr. One keypair per show, kind 10154 metadata, kind 54 episodes, and kind 10064 authorship claims. Complete manual page.">8<link rel="preload" href="../assets/fonts/michroma-400-latin.woff2" as="font" type="font/woff2" crossorigin>9<link rel="preload" href="../assets/fonts/exo2-var-latin.woff2" as="font" type="font/woff2" crossorigin>10<link rel="preload" href="../assets/fonts/sairacondensed-600-latin.woff2" as="font" type="font/woff2" crossorigin>11<link rel="preload" href="../assets/fonts/sairacondensed-700-latin.woff2" as="font" type="font/woff2" crossorigin>12<link rel="preload" href="../assets/fonts/sairacondensed-500-latin.woff2" as="font" type="font/woff2" crossorigin>13<link rel="preload" href="../assets/fonts/sharetechmono-400-latin.woff2" as="font" type="font/woff2" crossorigin>14<link rel="preload" href="../assets/fonts/exo2-400i-latin.woff2" as="font" type="font/woff2" crossorigin>15<link rel="icon" href="../assets/favicon.svg" type="image/svg+xml">16<link rel="stylesheet" href="../assets/manual.css">17</head>18<body>1920<svg width="0" height="0" style="position:absolute">21 <defs>22 <marker id="arr" markerWidth="9" markerHeight="9" refX="7" refY="4.5" orient="auto">23 <path d="M1,1 L8,4.5 L1,8 Z" fill="#f26a00"/>24 </marker>25 <marker id="arrb" markerWidth="9" markerHeight="9" refX="7" refY="4.5" orient="auto">26 <path d="M1,1 L8,4.5 L1,8 Z" fill="#2e5f9e"/>27 </marker>28 </defs>29</svg>3031<div class="page">32 <span class="staple s1"></span><span class="staple s2"></span>3334 <header class="band">35 <a class="logo" href="../index.html">NOSTR<b>▸</b>PROTOCOL MANUAL</a>36 <span class="booklet-note">Chapter F4 - Media</span>37 </header>38 <nav class="tabs">39 <a href="../index.html">Contents</a>40 <a href="#overview" class="on">Overview</a>41 <a href="#spec">Spec</a>42 <a href="#examples">Examples</a>43 <a href="#history">History</a>44 </nav>4546 <div class="chapter">47 <div class="nipno" data-n="F4">F4</div>48 <div class="titles">49 <div class="kicker">Nostr Implementation Possibility - Hex № F4</div>50 <h1>Podcasts</h1>51 <p class="sub">A podcast feed as signed events: one keypair per show, one event per episode, fetched from relays instead of an RSS URL.</p>52 </div>53 </div>5455 <main class="spread">56 <div class="main">5758 <h2 class="bar" id="overview"><span class="num">1</span> Overview</h2>59 <p>This NIP defines how podcast clients fetch podcast episodes from relays. The design fits into existing podcast players with little change. A feed stops being one RSS file at one URL. It becomes a set of Nostr events: one metadata event for the show, and one event per episode.</p>60 <p>RSS feeds have three structural problems. First, a feed depends on a URL. Hosting a URL is hard for most people, so podcasters use service providers. Providers charge money, seal shows into walled gardens as Spotify has done, and can censor podcasters or block migration. On Nostr a banned podcaster moves to other relays. The move is trivial as long as the podcaster holds the key, because clients can discover the new relay URLs.</p>61 <p>Second, an RSS feed loads only in full. There is no pagination and no filtering. Sync in RSS clients is slow and cumbersome, which also nudges people toward centralized services. This gap led to broken schemes such as <a href="https://podping.org/">Podping</a>, which a Nostr-native feed does not need. A single episode also cannot be referenced directly; it exists only inside the full list. So people share links to centralized platforms or to YouTube copies instead, which is an antipattern. On Nostr each episode is its own event with its own ID.</p>62 <p>Third, an RSS feed allows no interaction. Listeners cannot like an episode, comment on it, or signal that they listened. This pushes both listeners and podcasters toward closed platforms that offer engagement and analytics. On Nostr, episode events can receive <a href="25.html">NIP-25</a> reactions and <a href="22.html">NIP-22</a> comments like any other event.</p>6364 <figure class="diagram">65 <div class="frame">66 <svg viewBox="0 0 560 200" role="img" aria-label="Podcast key publishes metadata and episode events to relays; podcast players subscribe; audio files stay on a media server">67 <rect class="d-actor" x="10" y="70" width="120" height="54" rx="8"/>68 <text class="d-actor-label" x="70" y="93" text-anchor="middle">Podcast key</text>69 <text class="d-note" x="70" y="110" text-anchor="middle">signs 10154 + 54</text>70 <rect class="d-actor" x="225" y="20" width="110" height="44" rx="8"/>71 <text class="d-actor-label" x="280" y="47" text-anchor="middle">Relay 1</text>72 <rect class="d-actor" x="225" y="126" width="110" height="44" rx="8"/>73 <text class="d-actor-label" x="280" y="153" text-anchor="middle">Relay 2</text>74 <rect class="d-actor" x="440" y="70" width="110" height="54" rx="8"/>75 <text class="d-actor-label" x="495" y="93" text-anchor="middle">Player</text>76 <text class="d-note" x="495" y="110" text-anchor="middle">REQ + filter</text>77 <path class="d-msg d-flow" d="M132,86 C175,72 182,52 223,44"/>78 <path class="d-msg d-flow" d="M132,108 C175,122 182,140 223,148"/>79 <path class="d-msg blue d-flow" d="M337,44 C380,52 392,72 438,86"/>80 <path class="d-msg blue d-flow" d="M337,148 C380,140 392,122 438,108"/>81 <text class="d-label" x="163" y="46" text-anchor="middle">EVENT</text>82 <text class="d-label" x="398" y="46" text-anchor="middle">episodes</text>83 <text class="d-note" x="280" y="192" text-anchor="middle">Audio files live at plain HTTP URLs; events carry only the links.</text>84 <circle r="4" fill="#f26a00">85 <animateMotion dur="2.6s" repeatCount="indefinite" path="M132,86 C175,72 182,52 223,44"/>86 </circle>87 <circle r="4" fill="#2e5f9e">88 <animateMotion dur="2.6s" begin="1.3s" repeatCount="indefinite" path="M337,44 C380,52 392,72 438,86"/>89 </circle>90 </svg>91 </div>92 <figcaption>Fig. F4-1 — <b>A feed without a feed URL.</b> The podcast key publishes show metadata and episodes to relays. Players subscribe with normal <a href="01.html">NIP-01</a> filters.</figcaption>93 </figure>9495 <h2 class="bar" id="spec"><span class="num">2</span> Concept: One Keypair per Podcast</h2>96 <p>Each podcast is its own Nostr keypair. The podcast public key signs the show metadata and every episode. This is simple. It also lets a podcast combine its podcasting presence with a normal <code>kind:0</code> / <code>kind:1</code> microblogging presence under the same key.</p>97 <p>A shared keypair allows shared ownership. Co-hosts can hold the key together, via MuSig2 or a plain agreement between friends. Ownership can also change over time, based on human trust in how the key material is handled.</p>98 <div class="box hint">99 <span class="tag">Hint!</span>100 <p>The podcast key is the feed address. Share it as an <code>npub</code> or <code>nprofile</code> (<a href="19.html">NIP-19</a>) and any client can find the show on any relay that carries it.</p>101 </div>102103 <h2 class="bar" id="metadata"><span class="num">3</span> Podcast Metadata — Kind 10154</h2>104 <p>Show-level information lives in a <code>kind:10154</code> <strong>replaceable</strong> event (see the storage classes in <a href="01.html">NIP-01</a>). The podcast key signs it. It contains everything a podcast client needs to display the show.</p>105 <p>Similar information may exist in the podcast's <code>kind:0</code> profile. Podcast-specific clients should read directly from <code>kind:10154</code> and may ignore <code>kind:0</code> entirely.</p>106107 <div class="crt">108 <div class="titlebar">Wire format - kind 10154 podcast metadata</div>109<pre>{110 <span class="k">"id"</span>: <span class="s">"..."</span>,111 <span class="k">"pubkey"</span>: <span class="c">&lt;podcast-pubkey&gt;</span>,112 <span class="k">"kind"</span>: 10154,113 <span class="k">"created_at"</span>: 1700682555,114 <span class="k">"tags"</span>: [115 [<span class="s">"title"</span>, <span class="c">&lt;podcast-title&gt;</span>],116 [<span class="s">"image"</span>, <span class="c">&lt;podcast-cover-image-URL&gt;</span>],117 [<span class="s">"description"</span>, <span class="c">&lt;podcast-description&gt;</span>],118 [<span class="s">"website"</span>, <span class="c">&lt;podcast-website-URL&gt;</span>], <span class="c">// optional, multiple</span>119 [<span class="s">"p"</span>, <span class="c">&lt;podcast-author-pubkey&gt;</span>, <span class="c">&lt;role&gt;</span>] <span class="c">// optional, multiple</span>120 ],121 <span class="k">"content"</span>: <span class="s">""</span>,122 <span class="k">"sig"</span>: <span class="s">"..."</span>123}</pre>124 </div>125126 <div class="tscroll"><table class="ref">127 <caption>Kind 10154 tags</caption>128 <tr><th>Tag</th><th>Value</th><th>Rule</th></tr>129 <tr><td class="btn">title</td><td>show title</td><td>The name of the podcast.</td></tr>130 <tr><td class="btn">image</td><td>URL</td><td>The cover image of the podcast.</td></tr>131 <tr><td class="btn">description</td><td>text</td><td>A description of the podcast.</td></tr>132 <tr><td class="btn">website</td><td>URL</td><td>A website of the podcast. Optional. May appear multiple times.</td></tr>133 <tr><td class="btn">p</td><td>author pubkey + role</td><td>An author of the podcast. Optional. May appear multiple times. The third element is an optional role: <span class="kbd">host</span>, <span class="kbd">cohost</span>, or <span class="kbd">editor</span>.</td></tr>134 </table></div>135136 <h2 class="bar" id="authorship"><span class="num">4</span> Authored Podcasts — Kind 10064</h2>137 <p>Do not trust the <code>p</code> tags in <code>kind:10154</code> blindly. Any podcast can falsely claim any person as its author. Before a client displays the authorship, it must check the claim against an equivalent counter-claim signed by that author.</p>138 <p>The counter-claim is a <code>kind:10064</code> replaceable event, signed by the user's own key. In it, an author lists the public keys of the podcasts they author. Clients can also use this event in reverse: fetch it to discover all podcasts authored by a given user.</p>139140 <div class="crt">141 <div class="titlebar">Wire format - kind 10064 authored podcasts</div>142<pre>{143 <span class="k">"id"</span>: <span class="s">"..."</span>,144 <span class="k">"pubkey"</span>: <span class="c">&lt;user-pubkey&gt;</span>,145 <span class="k">"kind"</span>: 10064,146 <span class="k">"created_at"</span>: 1700682555,147 <span class="k">"tags"</span>: [148 [<span class="s">"p"</span>, <span class="c">&lt;podcast-pubkey-1&gt;</span>],149 [<span class="s">"p"</span>, <span class="c">&lt;podcast-pubkey-2&gt;</span>]150 ],151 <span class="k">"content"</span>: <span class="s">""</span>,152 <span class="k">"sig"</span>: <span class="s">"..."</span>153}</pre>154 </div>155156 <div class="box tech">157 <span class="tag">Tech Note</span>158 <p>The source spec contradicts itself here. The prose introduces this event as <code>kind:10164</code>, but the example shows <code>kind:10064</code>. The <a href="51.html">NIP-51</a> standard-lists table settles it: "Authored podcasts" is registered as kind <code>10064</code>. This page uses 10064 throughout; treat <code>10164</code> in the source as a typo.</p>159 </div>160161 <figure class="diagram">162 <div class="frame">163 <svg viewBox="0 0 560 210" role="img" aria-label="Client cross-checks the podcast's p-tag claim in kind 10154 against the author's kind 10064 counter-claim before displaying authorship">164 <rect class="d-actor" x="10" y="20" width="150" height="54" rx="8"/>165 <text class="d-actor-label" x="85" y="43" text-anchor="middle">Podcast key</text>166 <text class="d-note" x="85" y="60" text-anchor="middle">10154: "p" → Alice</text>167 <rect class="d-actor" x="10" y="130" width="150" height="54" rx="8"/>168 <text class="d-actor-label" x="85" y="153" text-anchor="middle">Alice's key</text>169 <text class="d-note" x="85" y="170" text-anchor="middle">10064: "p" → podcast</text>170 <rect class="d-actor" x="230" y="75" width="120" height="54" rx="8"/>171 <text class="d-actor-label" x="290" y="98" text-anchor="middle">Client</text>172 <text class="d-note" x="290" y="115" text-anchor="middle">match both?</text>173 <rect class="d-actor" x="420" y="75" width="130" height="54" rx="8"/>174 <text class="d-actor-label" x="485" y="98" text-anchor="middle">Display</text>175 <text class="d-note" x="485" y="115" text-anchor="middle">"Host: Alice"</text>176 <path class="d-msg d-flow" d="M162,47 C200,55 205,75 227,88"/>177 <path class="d-msg blue d-flow" d="M162,157 C200,149 205,129 227,116"/>178 <path class="d-msg d-flow" d="M352,102 L417,102"/>179 <text class="d-label" x="190" y="42" text-anchor="middle">claim</text>180 <text class="d-label" x="190" y="176" text-anchor="middle">counter-claim</text>181 <text class="d-label" x="385" y="94" text-anchor="middle">both agree</text>182 </svg>183 </div>184 <figcaption>Fig. F4-2 — <b>Authorship handshake.</b> The client shows an author only when the show's claim and the author's counter-claim point at each other.</figcaption>185 </figure>186187 <h2 class="bar" id="episodes"><span class="num">5</span> Podcast Episodes — Kind 54</h2>188 <p>Each episode is a <code>kind:54</code> event. The podcast public key authors it directly. Kind 54 is a <strong>regular</strong> event, so relays store every episode and clients can filter and paginate with <code>since</code>, <code>until</code>, and <code>limit</code>. The <code>content</code> holds the episode notes as Markdown. The audio itself stays at an external URL.</p>189190 <div class="crt">191 <div class="titlebar">Wire format - kind 54 podcast episode</div>192<pre>{193 <span class="k">"id"</span>: <span class="s">"..."</span>,194 <span class="k">"pubkey"</span>: <span class="c">&lt;podcast-pubkey&gt;</span>,195 <span class="k">"kind"</span>: 54,196 <span class="k">"created_at"</span>: 1700682555,197 <span class="k">"tags"</span>: [198 [<span class="s">"title"</span>, <span class="c">&lt;episode title&gt;</span>],199 [<span class="s">"image"</span>, <span class="c">&lt;optional episode image&gt;</span>],200 [<span class="s">"description"</span>, <span class="c">&lt;a brief description&gt;</span>],201 [<span class="s">"audio"</span>, <span class="c">&lt;audio-url&gt;</span>, <span class="c">&lt;optional_media_type&gt;</span>] <span class="c">// multiple allowed</span>202 ],203 <span class="k">"content"</span>: <span class="c">&lt;markdown content&gt;</span>,204 <span class="k">"sig"</span>: <span class="s">"..."</span>205}</pre>206 </div>207208 <div class="tscroll"><table class="ref">209 <caption>Kind 54 tags</caption>210 <tr><th>Tag</th><th>Value</th><th>Rule</th></tr>211 <tr><td class="btn">title</td><td>text</td><td>The episode title.</td></tr>212 <tr><td class="btn">image</td><td>URL</td><td>An episode image. Optional.</td></tr>213 <tr><td class="btn">description</td><td>text</td><td>A brief description of the episode.</td></tr>214 <tr><td class="btn">audio</td><td>URL + media type</td><td>The audio file. The second element is an optional media type (for example <code>audio/mpeg</code>). May appear multiple times for alternative encodings.</td></tr>215 </table></div>216217 <div class="box tech">218 <span class="tag">Tech Note</span>219 <p>The episode tag list is explicitly open-ended. The source says "other important fields to be specified here later after further discovery". Common RSS fields such as duration, episode number, season, and transcript have no tags yet. Expect this list to grow while the NIP is a draft.</p>220 </div>221222 <h2 class="bar" id="favorites"><span class="num">6</span> Favorite Podcasts — Kind 10054</h2>223 <p>Listeners can publicly advertise the podcasts they listen to, as a soft recommendation. This is the <code>kind:10054</code> "Favorite podcasts" list defined in <a href="51.html">NIP-51</a>. Per that NIP it holds <code>p</code> tags with podcast pubkeys and <code>url</code> tags with RSS/XML podcast URLs. See <a href="51.html">NIP-51</a> for the full list rules.</p>224225 <h2 class="bar" id="examples"><span class="num">7</span> Worked Example</h2>226 <p>A player follows the show behind pubkey <code>7f9edc21…</code>. It fetches show metadata, the latest episodes, and the authorship proof in one <code>REQ</code> each. Orange lines go to the relay. Blue lines come back.</p>227228 <div class="crt">229 <div class="titlebar">Session transcript - wss://relay.example.com</div>230<pre><span class="c">-- fetch the show card --</span>231<span class="send">→ ["REQ", "show", {"kinds":[10154], "authors":["7f9edc21…"]}]</span>232<span class="recv">← ["EVENT", "show", {"kind":10154,"pubkey":"7f9edc21…","tags":[233 ["title","Signals from Nowhere"],234 ["image","https://cdn.example.com/cover.jpg"],235 ["description","Weekly radio noise from the edge of the mesh."],236 ["website","https://signals.example.com"],237 ["p","a1b2c3d4…","host"]], "content":"", …}]</span>238<span class="recv">← ["EOSE", "show"]</span>239240<span class="c">-- fetch the 2 newest episodes (pagination via limit/until) --</span>241<span class="send">→ ["REQ", "eps", {"kinds":[54], "authors":["7f9edc21…"], "limit":2}]</span>242<span class="recv">← ["EVENT", "eps", {"kind":54,"pubkey":"7f9edc21…","created_at":1749762600,243 "tags":[["title","E23: Antennas"],244 ["description","We climb the roof."],245 ["audio","https://cdn.example.com/e23.mp3","audio/mpeg"],246 ["audio","https://cdn.example.com/e23.ogg","audio/ogg"]],247 "content":"Show notes in **markdown**.", …}]</span>248<span class="recv">← ["EVENT", "eps", {"kind":54, "tags":[["title","E22: Ground Loops"], …], …}]</span>249<span class="recv">← ["EOSE", "eps"]</span>250251<span class="c">-- verify the host claim before displaying it --</span>252<span class="send">→ ["REQ", "auth", {"kinds":[10064], "authors":["a1b2c3d4…"]}]</span>253<span class="recv">← ["EVENT", "auth", {"kind":10064,"pubkey":"a1b2c3d4…",254 "tags":[["p","7f9edc21…"]], …}]</span>255<span class="c">-- counter-claim matches: the client may now show "Host: a1b2c3d4…" --</span></pre>256 </div>257258 <h2 class="bar" id="history"><span class="num">8</span> Version History</h2>259 <p>NIP-F4 is young. It landed in one commit and has not changed since:</p>260 <ul class="patchnotes">261 <li><span class="ver">Origin</span><span class="when">2026-05</span>fiatjaf published the proposal as "NIP-F4: podcasts" in PR #1093. Kinds 10154, 10064, and 54 defined in the initial text; the matching 10054 and 10064 rows were registered in <a href="51.html">NIP-51</a>.</li>262 </ul>263264 <div class="credits">265 <h3 class="cardhead">Original Proposal - Credits</h3>266 <table>267 <tr><td>Author</td><td>fiatjaf</td></tr>268 <tr><td>First commit</td><td>2026-05-28 — “NIP-F4: podcasts (#1093)”</td></tr>269 <tr><td>Pull request</td><td><a href="https://github.com/nostr-protocol/nips/pull/1093">nostr-protocol/nips #1093</a></td></tr>270 <tr><td>Status</td><td>draft - optional</td></tr>271 <tr><td>Source</td><td><a href="https://github.com/nostr-protocol/nips/blob/master/F4.md">F4.md on GitHub</a> - <a href="https://github.com/nostr-protocol/nips/commits/master/F4.md">full change history</a></td></tr>272 </table>273 </div>274275 <h2 class="bar"><span class="num">9</span> See Also</h2>276 <ul>277 <li><a href="01.html">NIP-01</a> — the base protocol: events, filters, and the replaceable/regular storage classes used here.</li>278 <li><a href="51.html">NIP-51</a> — lists; defines the kind 10054 favorite-podcasts list and registers kind 10064.</li>279 <li><a href="19.html">NIP-19</a> — <code>npub</code>/<code>nprofile</code> encodings for sharing a podcast key with its relays.</li>280 <li><a href="25.html">NIP-25</a> — reactions; lets listeners like an episode event.</li>281 <li><a href="22.html">NIP-22</a> — comments on events of any kind, including episodes.</li>282 <li><a href="65.html">NIP-65</a> — relay lists; how clients discover where a podcast key publishes.</li>283 <li><a href="a0.html">NIP-A0</a> — voice messages; a related audio-over-Nostr event format.</li>284 </ul>285 </div>286287 <aside>288 <div class="cartridge">289 <div class="inner">290 <h3 class="cardhead">Spec Card</h3>291 <div class="lights"><span class="led on"></span><span class="led warn"></span><span class="led warn"></span></div>292 <dl>293 <dt>Status</dt><dd><span class="chip draft">draft</span><span class="chip">optional</span></dd>294 <dt>Defines</dt><dd>show metadata - authorship claims - episode events</dd>295 <dt>Kinds</dt><dd>54 (episode) - 10154 (metadata) - 10064 (authored) - 10054 (via <a href="51.html">NIP-51</a>)</dd>296 <dt>Requires</dt><dd><a href="01.html">NIP-01</a> - <a href="51.html">NIP-51</a></dd>297 <dt>Media</dt><dd>audio at external HTTP URLs</dd>298 <dt>First commit</dt><dd>2026-05-28</dd>299 <dt>Last updated</dt><dd>2026-05-28</dd>300 </dl>301 </div>302 </div>303 </aside>304 </main>305306 <nav class="navbtns">307 <a href="ee.html">◀ NIP-EE: E2EE Messaging (MLS)</a>308 <a href="../index.html">Contents ▶</a>309 </nav>310311 <footer class="pagefoot">312 <a href="../index.html">Nostr Protocol Manual</a>313 <span class="pageno">NIP F4 / F4</span>314 </footer>315</div>316317<script src="../assets/webmcp.js" defer></script>318</body>319</html>