12<html lang="en">3<head>4<meta charset="utf-8">5<meta name="viewport" content="width=device-width, initial-scale=1">6<title>NIP-68 - Picture-first feeds — Nostr Instruction Manual</title>7<meta name="description" content="NIP-68: kind 20 picture events for Instagram-style Nostr clients. Self-contained image posts with imeta tags, user annotations, and media-type filters.">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 68 - 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="68">68</div>48 <div class="titles">49 <div class="kicker">Nostr Implementation Possibility - Hex № 68</div>50 <h1>Picture-first feeds</h1>51 <p class="sub">Kind 20 gives image posts their own event type, so a client can build a feed where the picture takes center stage.</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>Some clients want to look like Instagram, Flickr, Snapchat, or 9GAG. In those apps the picture is the post. Text is secondary. This NIP defines event kind <code>20</code>, the <strong>picture event</strong>, for exactly that use case.</p>60 <p>A picture event must be <strong>self-contained</strong>. The relay does not store image bytes. The images are hosted externally. The event references each image with an <code>imeta</code> tag (see <a href="92.html">NIP-92</a>). Each <code>imeta</code> tag carries the URL, the media type, dimensions, hashes, alt text, and fallback URLs. A client can render the whole post from the event alone, then fetch the pixels.</p>61 <p>One picture event may reference multiple images. A client displays them together as a single post, like a photo carousel. Picture feeds can also mix in <a href="71.html">NIP-71</a> kind <code>22</code> events to show short vertical videos in the same stream.</p>6263 <div class="box hint">64 <span class="tag">Hint!</span>65 <p>Why a new kind and not a kind-1 note with an image link? A dedicated kind lets clients filter for picture posts directly: <code>{"kinds": [20]}</code>. No text-note noise, no guessing which URLs are images.</p>66 </div>6768 <figure class="diagram">69 <div class="frame">70 <svg viewBox="0 0 560 200" role="img" aria-label="Client A publishes a kind 20 event to a relay; Client B subscribes with kinds 20 and fetches the image bytes from an external media host">71 <rect class="d-actor" x="10" y="70" width="110" height="50" rx="8"/>72 <text class="d-actor-label" x="65" y="99" text-anchor="middle">Client A</text>73 <rect class="d-actor" x="225" y="20" width="110" height="44" rx="8"/>74 <text class="d-actor-label" x="280" y="47" text-anchor="middle">Relay</text>75 <rect class="d-actor" x="225" y="136" width="110" height="44" rx="8"/>76 <text class="d-actor-label" x="280" y="163" text-anchor="middle">Media host</text>77 <rect class="d-actor" x="440" y="70" width="110" height="50" rx="8"/>78 <text class="d-actor-label" x="495" y="99" text-anchor="middle">Client B</text>79 <path class="d-msg d-flow" d="M122,84 C170,70 180,52 223,44"/>80 <path class="d-msg blue d-flow" d="M337,44 C380,52 390,70 438,84"/>81 <path class="d-msg d-flow" d="M493,122 C470,150 390,158 339,158"/>82 <text class="d-label" x="150" y="46" text-anchor="middle">EVENT kind 20</text>83 <text class="d-label" x="410" y="46" text-anchor="middle">REQ kinds:[20]</text>84 <text class="d-label" x="440" y="160" text-anchor="middle">GET image.jpg</text>85 <text class="d-note" x="65" y="140" text-anchor="middle">uploads first,</text>86 <text class="d-note" x="65" y="154" text-anchor="middle">then publishes</text>87 <circle r="4" fill="#f26a00">88 <animateMotion dur="2.6s" repeatCount="indefinite" path="M122,84 C170,70 180,52 223,44"/>89 </circle>90 <circle r="4" fill="#2e5f9e">91 <animateMotion dur="2.6s" begin="1.3s" repeatCount="indefinite" path="M337,44 C380,52 390,70 438,84"/>92 </circle>93 </svg>94 </div>95 <figcaption>Fig. 68-1 — <b>Metadata on the relay, pixels elsewhere.</b> The relay carries the kind-20 event; the client fetches image bytes from the URL in the <code>imeta</code> tag.</figcaption>96 </figure>9798 <h2 class="bar" id="spec"><span class="num">2</span> The Picture Event (kind 20)</h2>99 <p>A picture event has a <code>title</code> tag and a description in <code>content</code>. It follows the standard <a href="01.html">NIP-01</a> event format. Kind <code>20</code> falls in the regular range, so relays store every picture event.</p>100101 <div class="crt">102 <div class="titlebar">Wire format - picture event, kind 20</div>103<pre>{104 <span class="k">"id"</span>: <span class="c"><32-byte lowercase hex sha256 of the serialized event></span>,105 <span class="k">"pubkey"</span>: <span class="c"><32-byte lowercase hex public key of the creator></span>,106 <span class="k">"created_at"</span>: <span class="c"><unix timestamp, seconds></span>,107 <span class="k">"kind"</span>: 20,108 <span class="k">"content"</span>: <span class="s">"<description of the post>"</span>,109 <span class="k">"tags"</span>: [110 [<span class="s">"title"</span>, <span class="s">"<short title of the post>"</span>],111112 <span class="c">// one imeta tag per image (see §3)</span>113 [<span class="s">"imeta"</span>, <span class="s">"url …"</span>, <span class="s">"m image/jpeg"</span>, <span class="s">"thumbhash …"</span>, <span class="s">"blurhash …"</span>,114 <span class="s">"dim 3024x4032"</span>, <span class="s">"alt …"</span>, <span class="s">"x <sha256>"</span>, <span class="s">"fallback …"</span>, <span class="s">"fallback …"</span>],115 [<span class="s">"imeta"</span>, <span class="s">"url …"</span>, <span class="c">…</span>, <span class="s">"annotate-user <pubkey>:<posX>:<posY>"</span>],116117 [<span class="s">"content-warning"</span>, <span class="s">"<reason>"</span>], <span class="c">// if NSFW</span>118119 [<span class="s">"p"</span>, <span class="s">"<pubkey>"</span>, <span class="s">"<relay URL, optional>"</span>], <span class="c">// tagged users</span>120121 [<span class="s">"m"</span>, <span class="s">"image/jpeg"</span>], <span class="c">// media type, for filters</span>122 [<span class="s">"x"</span>, <span class="s">"<sha256>"</span>], <span class="c">// hash of each image, queryable</span>123124 [<span class="s">"t"</span>, <span class="s">"<hashtag>"</span>],125126 [<span class="s">"location"</span>, <span class="s">"<location>"</span>], <span class="c">// city, state, country</span>127 [<span class="s">"g"</span>, <span class="s">"<geohash>"</span>],128129 [<span class="s">"L"</span>, <span class="s">"ISO-639-1"</span>], <span class="c">// language of text in the image</span>130 [<span class="s">"l"</span>, <span class="s">"en"</span>, <span class="s">"ISO-639-1"</span>]131 ]132}</pre>133 </div>134135 <div class="tscroll"><table class="ref">136 <caption>Event-level tags</caption>137 <tr><th>Tag</th><th>Value</th><th>Rule</th></tr>138 <tr><td class="btn">title</td><td>string</td><td>Short title of the post. The long description lives in <code>content</code>.</td></tr>139 <tr><td class="btn">imeta</td><td>key-value pairs</td><td>One per image. Carries all metadata for that image. See §3.</td></tr>140 <tr><td class="btn">content-warning</td><td>reason</td><td>Add this tag when the post is NSFW. Clients hide the picture until the user opts in (see <a href="36.html">NIP-36</a>).</td></tr>141 <tr><td class="btn">p</td><td>pubkey, optional relay URL</td><td>A tagged user. One tag per user, as in <a href="01.html">NIP-01</a>.</td></tr>142 <tr><td class="btn">m</td><td>media type</td><td>Repeats the media type at event level. Lets clients filter with <code>{"#m": [...]}</code> for kinds they can render.</td></tr>143 <tr><td class="btn">x</td><td>sha256 hex</td><td>Repeats each image hash at event level. Makes images queryable with <code>{"#x": [...]}</code>.</td></tr>144 <tr><td class="btn">t</td><td>hashtag</td><td>A hashtag. One tag per hashtag.</td></tr>145 <tr><td class="btn">location</td><td>location string</td><td>Human-readable place: city name, state, country.</td></tr>146 <tr><td class="btn">g</td><td>geohash</td><td>Machine-readable position of the post.</td></tr>147 <tr><td class="btn">L</td><td>ISO-639-1</td><td>Label namespace (see <a href="32.html">NIP-32</a>). Add it when text is written in the image.</td></tr>148 <tr><td class="btn">l</td><td>language code, ISO-639-1</td><td>The language of the text in the image, for example <code>en</code>.</td></tr>149 </table></div>150151 <h2 class="bar" id="imeta"><span class="num">3</span> The imeta Tag, Per Image</h2>152 <p>Each image gets one <code>imeta</code> tag. Every element after the tag name is a space-separated key-value string. The format comes from <a href="92.html">NIP-92</a>; the hash and metadata fields come from <a href="94.html">NIP-94</a>.</p>153154 <div class="tscroll"><table class="ref">155 <caption>imeta properties</caption>156 <tr><th>Key</th><th>Value</th><th>Rule</th></tr>157 <tr><td class="btn">url</td><td>image URL</td><td>The primary location of the image on an external host.</td></tr>158 <tr><td class="btn">m</td><td>media type</td><td>MIME type of the image. Must be one of the six accepted types in §4.</td></tr>159 <tr><td class="btn">thumbhash</td><td>base64 string</td><td>A compact placeholder. Clients render it while the image loads.</td></tr>160 <tr><td class="btn">blurhash</td><td>blurhash string</td><td>An alternative compact placeholder with the same purpose.</td></tr>161 <tr><td class="btn">dim</td><td>WxH</td><td>Pixel dimensions, for example <code>3024x4032</code>. Prevents layout shift.</td></tr>162 <tr><td class="btn">alt</td><td>string</td><td>Accessibility description of the image.</td></tr>163 <tr><td class="btn">x</td><td>sha256 hex</td><td>Hash of the image file, as specified in <a href="94.html">NIP-94</a>. Verifies the download.</td></tr>164 <tr><td class="btn">fallback</td><td>image URL</td><td>A mirror of the same file. The key may repeat for multiple mirrors.</td></tr>165 <tr><td class="btn">annotate-user</td><td><code><pubkey>:<posX>:<posY></code></td><td>Places a user link at a specific position in the picture. The pubkey is 32-byte hex.</td></tr>166 </table></div>167168 <figure class="diagram">169 <div class="frame">170 <svg viewBox="0 0 560 150" role="img" aria-label="Client tries the primary url, then each fallback mirror in order, and verifies the bytes against the x hash">171 <rect class="d-actor" x="10" y="50" width="100" height="50" rx="8"/>172 <text class="d-actor-label" x="60" y="72" text-anchor="middle">Client</text>173 <text class="d-note" x="60" y="89" text-anchor="middle">reads imeta</text>174 <rect class="d-actor" x="170" y="14" width="130" height="40" rx="8"/>175 <text class="d-actor-label" x="235" y="39" text-anchor="middle">url</text>176 <rect class="d-actor" x="170" y="66" width="130" height="40" rx="8"/>177 <text class="d-actor-label" x="235" y="91" text-anchor="middle">fallback 1</text>178 <rect class="d-actor" x="170" y="118" width="130" height="26" rx="8"/>179 <text class="d-actor-label" x="235" y="136" text-anchor="middle">fallback 2</text>180 <rect class="d-actor" x="420" y="50" width="130" height="50" rx="8"/>181 <text class="d-actor-label" x="485" y="72" text-anchor="middle">sha256 check</text>182 <text class="d-note" x="485" y="89" text-anchor="middle">bytes == x ?</text>183 <path class="d-msg d-flow" d="M112,62 C140,52 145,42 167,36"/>184 <path class="d-msg" d="M112,80 L167,86"/>185 <path class="d-msg" d="M112,95 C140,110 145,122 167,128"/>186 <path class="d-msg blue d-flow" d="M302,36 C360,42 380,56 417,66"/>187 <text class="d-note" x="345" y="30" text-anchor="middle">image bytes</text>188 <text class="d-note" x="140" y="120" text-anchor="middle">try in order</text>189 <circle r="4" fill="#2e5f9e">190 <animateMotion dur="2.6s" repeatCount="indefinite" path="M302,36 C360,42 380,56 417,66"/>191 </circle>192 </svg>193 </div>194 <figcaption>Fig. 68-2 — <b>Fetch and verify.</b> Try <code>url</code> first, then each <code>fallback</code> mirror. The <code>x</code> hash proves every mirror served the same file.</figcaption>195 </figure>196197 <h2 class="bar" id="mediatypes"><span class="num">4</span> Accepted Media Types</h2>198 <p>Picture events accept only the following media types. Reject other types.</p>199 <div class="tscroll"><table class="ref">200 <caption>Accepted media types (6)</caption>201 <tr><th>MIME type</th><th>Format</th></tr>202 <tr><td class="btn">image/apng</td><td>Animated Portable Network Graphics (APNG)</td></tr>203 <tr><td class="btn">image/avif</td><td>AV1 Image File Format (AVIF)</td></tr>204 <tr><td class="btn">image/gif</td><td>Graphics Interchange Format (GIF)</td></tr>205 <tr><td class="btn">image/jpeg</td><td>Joint Photographic Expert Group image (JPEG)</td></tr>206 <tr><td class="btn">image/png</td><td>Portable Network Graphics (PNG)</td></tr>207 <tr><td class="btn">image/webp</td><td>Web Picture format (WEBP)</td></tr>208 </table></div>209210 <div class="box warn">211 <span class="tag">Warning!</span>212 <p>No <code>image/svg+xml</code>, no <code>video/*</code>. For short vertical videos in the same feed, use <a href="71.html">NIP-71</a> kind <code>22</code> events instead.</p>213 </div>214215 <div class="box tech">216 <span class="tag">Tech Note</span>217 <p>The schema block in the source spec is fenced as YAML but written as JSON with comments, and it is missing a comma after the event-level <code>x</code> tag. Treat it as illustrative, not parseable. The source also uses <code>imeta</code> without citing <a href="92.html">NIP-92</a>, where the tag is defined.</p>218 </div>219220 <h2 class="bar" id="examples"><span class="num">5</span> Worked Example</h2>221 <p>A photographer posts two coast photos as one carousel. The second photo tags a friend standing in the frame. The event is publishable as-is: valid JSON, all metadata inline.</p>222223 <div class="crt">224 <div class="titlebar">Picture event - two-image post</div>225<pre>{226 <span class="k">"id"</span>: <span class="s">"7ce4b58c21…"</span>,227 <span class="k">"pubkey"</span>: <span class="s">"a3d1e0f492…"</span>,228 <span class="k">"created_at"</span>: 1749762600,229 <span class="k">"kind"</span>: 20,230 <span class="k">"content"</span>: <span class="s">"Two mornings on the Pacific side. No filter."</span>,231 <span class="k">"tags"</span>: [232 [<span class="s">"title"</span>, <span class="s">"Costa Rica coast"</span>],233 [<span class="s">"imeta"</span>,234 <span class="s">"url https://nostr.build/i/my-image.jpg"</span>,235 <span class="s">"m image/jpeg"</span>,236 <span class="s">"thumbhash zOcFFIIKmXdCinaXaHcmcHUFSA=="</span>,237 <span class="s">"blurhash eVF$^OI:${M{o#*0-nNFxakD-?xVM}WEWB%iNKxvR-oetmo#R-aen$"</span>,238 <span class="s">"dim 3024x4032"</span>,239 <span class="s">"alt A scenic photo overlooking the coast of Costa Rica"</span>,240 <span class="s">"x 8a2f04c1…"</span>,241 <span class="s">"fallback https://nostrcheck.me/alt1.jpg"</span>,242 <span class="s">"fallback https://void.cat/alt1.jpg"</span>],243 [<span class="s">"imeta"</span>,244 <span class="s">"url https://nostr.build/i/my-image2.jpg"</span>,245 <span class="s">"m image/jpeg"</span>,246 <span class="s">"thumbhash zOcFFIIKmXgzmWaXWIcmcFQEGA"</span>,247 <span class="s">"dim 3024x4032"</span>,248 <span class="s">"alt Another scenic photo overlooking the coast of Costa Rica"</span>,249 <span class="s">"x 61bb90d3…"</span>,250 <span class="s">"fallback https://nostrcheck.me/alt2.jpg"</span>,251 <span class="s">"fallback https://void.cat/alt2.jpg"</span>,252 <span class="s">"annotate-user f7234bd4c1…:0.62:0.41"</span>],253 [<span class="s">"p"</span>, <span class="s">"f7234bd4c1…"</span>, <span class="s">"wss://relay.example.com"</span>],254 [<span class="s">"m"</span>, <span class="s">"image/jpeg"</span>],255 [<span class="s">"x"</span>, <span class="s">"8a2f04c1…"</span>],256 [<span class="s">"x"</span>, <span class="s">"61bb90d3…"</span>],257 [<span class="s">"t"</span>, <span class="s">"photography"</span>],258 [<span class="s">"t"</span>, <span class="s">"costarica"</span>],259 [<span class="s">"location"</span>, <span class="s">"Santa Teresa, Puntarenas, Costa Rica"</span>],260 [<span class="s">"g"</span>, <span class="s">"d1u0q"</span>]261 ],262 <span class="k">"sig"</span>: <span class="s">"90ab44cd12…"</span>263}</pre>264 </div>265266 <p>A picture-first client builds its feed with one subscription. It asks for pictures and short videos together:</p>267 <div class="crt">268 <div class="titlebar">Session transcript - building the feed</div>269<pre><span class="send">→ ["REQ", "picfeed", {"kinds":[20,22], "limit":30}]</span>270<span class="recv">← ["EVENT", "picfeed", {"kind":20, "tags":[["title","Costa Rica coast"], …], …}]</span>271<span class="recv">← ["EVENT", "picfeed", {"kind":22, …}] <span class="c">// NIP-71 short video, same feed</span></span>272<span class="recv">← ["EOSE", "picfeed"]</span>273<span class="c">-- only JPEG-capable? filter by media type instead --</span>274<span class="send">→ ["REQ", "jpegs", {"kinds":[20], "#m":["image/jpeg"], "limit":30}]</span>275<span class="c">-- find every post that contains one exact image file --</span>276<span class="send">→ ["REQ", "byhash", {"kinds":[20], "#x":["8a2f04c1…"]}]</span></pre>277 </div>278279 <h2 class="bar" id="history"><span class="num">6</span> Version History</h2>280 <p>NIP-68 arrived in late 2024 and stabilized within weeks. Notable changes, newest first:</p>281 <ul class="patchnotes">282 <li><span class="ver">2026</span><span class="when">2026-04</span>Documented the optional <code>thumbhash</code> placeholder alongside <code>blurhash</code>.</li>283 <li><span class="ver">2025</span><span class="when">2025-01</span>Video companion updated: <a href="71.html">NIP-71</a> made video events regular, and the same-feed reference became kind <code>22</code>.</li>284 <li><span class="ver">2024</span><span class="when">2024-11</span>Added the <code>x</code> tag so posts are indexable by image sha256; simplified the text.</li>285 <li><span class="ver">2024</span><span class="when">2024-10</span>Added <code>location</code> and geohash tags, and <code>L</code>/<code>l</code> language labels for text written in images.</li>286 <li class="minor"><span class="ver">2024</span><span class="when">2024-10</span>Restricted posts to the six accepted media types.</li>287 <li><span class="ver">Origin</span><span class="when">2024-10-27</span>Vitor Pamplona proposed kind 20 under the working title "Instagram feeds".</li>288 </ul>289290 <div class="credits">291 <h3 class="cardhead">Original Proposal - Credits</h3>292 <table>293 <tr><td>Author</td><td>Vitor Pamplona</td></tr>294 <tr><td>First commit</td><td>2024-10-27 — “Instagram feeds”</td></tr>295 <tr><td>Status</td><td>draft - optional — only picture-first clients need it</td></tr>296 <tr><td>Source</td><td><a href="https://github.com/nostr-protocol/nips/blob/master/68.md">68.md on GitHub</a> - <a href="https://github.com/nostr-protocol/nips/commits/master/68.md">full change history</a></td></tr>297 </table>298 </div>299300 <h2 class="bar"><span class="num">7</span> See Also</h2>301 <ul>302 <li><a href="01.html">NIP-01</a> — the base event format, kinds, and the <code>p</code> tag used here.</li>303 <li><a href="92.html">NIP-92</a> — defines the <code>imeta</code> tag that carries all per-image metadata.</li>304 <li><a href="94.html">NIP-94</a> — file metadata; the source of the <code>x</code> sha256 hash convention.</li>305 <li><a href="71.html">NIP-71</a> — video events; kind <code>22</code> short videos share the picture feed.</li>306 <li><a href="36.html">NIP-36</a> — sensitive content; the <code>content-warning</code> tag used for NSFW posts.</li>307 <li><a href="32.html">NIP-32</a> — labeling; the <code>L</code>/<code>l</code> namespace used for image languages.</li>308 </ul>309 </div>310311 <aside>312 <div class="cartridge">313 <div class="inner">314 <h3 class="cardhead">Spec Card</h3>315 <div class="lights"><span class="led on"></span><span class="led on"></span><span class="led on"></span></div>316 <dl>317 <dt>Status</dt><dd><span class="chip draft">draft</span><span class="chip">optional</span></dd>318 <dt>Defines</dt><dd>kind 20 picture events - annotate-user - media-type allowlist</dd>319 <dt>Kind</dt><dd>20 (regular)</dd>320 <dt>Requires</dt><dd><a href="01.html">NIP-01</a> - <a href="92.html">NIP-92</a> - <a href="94.html">NIP-94</a></dd>321 <dt>Media</dt><dd>APNG - AVIF - GIF - JPEG - PNG - WEBP</dd>322 <dt>First commit</dt><dd>2024-10-27</dd>323 <dt>Last updated</dt><dd>2026-06-13</dd>324 </dl>325 </div>326 </div>327 </aside>328 </main>329330 <nav class="navbtns">331 <a href="67.html">◀ NIP-67: EOSE Completeness Hint</a>332 <a href="69.html">NIP-69: Peer-to-peer Order events ▶</a>333 </nav>334335 <footer class="pagefoot">336 <a href="../index.html">Nostr Protocol Manual</a>337 <span class="pageno">NIP 68 / F4</span>338 </footer>339</div>340341<script src="../assets/webmcp.js" defer></script>342</body>343</html>