Chapter 68 - Media
68
Nostr Implementation Possibility - Hex № 68

Picture-first feeds

Kind 20 gives image posts their own event type, so a client can build a feed where the picture takes center stage.

1 Overview

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 20, the picture event, for exactly that use case.

A picture event must be self-contained. The relay does not store image bytes. The images are hosted externally. The event references each image with an imeta tag (see NIP-92). Each imeta 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.

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 NIP-71 kind 22 events to show short vertical videos in the same stream.

Hint!

Why a new kind and not a kind-1 note with an image link? A dedicated kind lets clients filter for picture posts directly: {"kinds": [20]}. No text-note noise, no guessing which URLs are images.

Client A Relay Media host Client B EVENT kind 20 REQ kinds:[20] GET image.jpg uploads first, then publishes
Fig. 68-1 — Metadata on the relay, pixels elsewhere. The relay carries the kind-20 event; the client fetches image bytes from the URL in the imeta tag.

2 The Picture Event (kind 20)

A picture event has a title tag and a description in content. It follows the standard NIP-01 event format. Kind 20 falls in the regular range, so relays store every picture event.

Wire format - picture event, kind 20
{
  "id":         <32-byte lowercase hex sha256 of the serialized event>,
  "pubkey":     <32-byte lowercase hex public key of the creator>,
  "created_at": <unix timestamp, seconds>,
  "kind":       20,
  "content":    "<description of the post>",
  "tags": [
    ["title", "<short title of the post>"],

    // one imeta tag per image (see §3)
    ["imeta", "url …", "m image/jpeg", "thumbhash …", "blurhash …",
     "dim 3024x4032", "alt …", "x <sha256>", "fallback …", "fallback …"],
    ["imeta", "url …", …, "annotate-user <pubkey>:<posX>:<posY>"],

    ["content-warning", "<reason>"],          // if NSFW

    ["p", "<pubkey>", "<relay URL, optional>"], // tagged users

    ["m", "image/jpeg"],                       // media type, for filters
    ["x", "<sha256>"],                          // hash of each image, queryable

    ["t", "<hashtag>"],

    ["location", "<location>"],                // city, state, country
    ["g", "<geohash>"],

    ["L", "ISO-639-1"],                         // language of text in the image
    ["l", "en", "ISO-639-1"]
  ]
}
Event-level tags
TagValueRule
titlestringShort title of the post. The long description lives in content.
imetakey-value pairsOne per image. Carries all metadata for that image. See §3.
content-warningreasonAdd this tag when the post is NSFW. Clients hide the picture until the user opts in (see NIP-36).
ppubkey, optional relay URLA tagged user. One tag per user, as in NIP-01.
mmedia typeRepeats the media type at event level. Lets clients filter with {"#m": [...]} for kinds they can render.
xsha256 hexRepeats each image hash at event level. Makes images queryable with {"#x": [...]}.
thashtagA hashtag. One tag per hashtag.
locationlocation stringHuman-readable place: city name, state, country.
ggeohashMachine-readable position of the post.
LISO-639-1Label namespace (see NIP-32). Add it when text is written in the image.
llanguage code, ISO-639-1The language of the text in the image, for example en.

3 The imeta Tag, Per Image

Each image gets one imeta tag. Every element after the tag name is a space-separated key-value string. The format comes from NIP-92; the hash and metadata fields come from NIP-94.

imeta properties
KeyValueRule
urlimage URLThe primary location of the image on an external host.
mmedia typeMIME type of the image. Must be one of the six accepted types in §4.
thumbhashbase64 stringA compact placeholder. Clients render it while the image loads.
blurhashblurhash stringAn alternative compact placeholder with the same purpose.
dimWxHPixel dimensions, for example 3024x4032. Prevents layout shift.
altstringAccessibility description of the image.
xsha256 hexHash of the image file, as specified in NIP-94. Verifies the download.
fallbackimage URLA mirror of the same file. The key may repeat for multiple mirrors.
annotate-user<pubkey>:<posX>:<posY>Places a user link at a specific position in the picture. The pubkey is 32-byte hex.
Client reads imeta url fallback 1 fallback 2 sha256 check bytes == x ? image bytes try in order
Fig. 68-2 — Fetch and verify. Try url first, then each fallback mirror. The x hash proves every mirror served the same file.

4 Accepted Media Types

Picture events accept only the following media types. Reject other types.

Accepted media types (6)
MIME typeFormat
image/apngAnimated Portable Network Graphics (APNG)
image/avifAV1 Image File Format (AVIF)
image/gifGraphics Interchange Format (GIF)
image/jpegJoint Photographic Expert Group image (JPEG)
image/pngPortable Network Graphics (PNG)
image/webpWeb Picture format (WEBP)
Warning!

No image/svg+xml, no video/*. For short vertical videos in the same feed, use NIP-71 kind 22 events instead.

Tech Note

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 x tag. Treat it as illustrative, not parseable. The source also uses imeta without citing NIP-92, where the tag is defined.

5 Worked Example

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.

Picture event - two-image post
{
  "id": "7ce4b58c21…",
  "pubkey": "a3d1e0f492…",
  "created_at": 1749762600,
  "kind": 20,
  "content": "Two mornings on the Pacific side. No filter.",
  "tags": [
    ["title", "Costa Rica coast"],
    ["imeta",
      "url https://nostr.build/i/my-image.jpg",
      "m image/jpeg",
      "thumbhash zOcFFIIKmXdCinaXaHcmcHUFSA==",
      "blurhash eVF$^OI:${M{o#*0-nNFxakD-?xVM}WEWB%iNKxvR-oetmo#R-aen$",
      "dim 3024x4032",
      "alt A scenic photo overlooking the coast of Costa Rica",
      "x 8a2f04c1…",
      "fallback https://nostrcheck.me/alt1.jpg",
      "fallback https://void.cat/alt1.jpg"],
    ["imeta",
      "url https://nostr.build/i/my-image2.jpg",
      "m image/jpeg",
      "thumbhash zOcFFIIKmXgzmWaXWIcmcFQEGA",
      "dim 3024x4032",
      "alt Another scenic photo overlooking the coast of Costa Rica",
      "x 61bb90d3…",
      "fallback https://nostrcheck.me/alt2.jpg",
      "fallback https://void.cat/alt2.jpg",
      "annotate-user f7234bd4c1…:0.62:0.41"],
    ["p", "f7234bd4c1…", "wss://relay.example.com"],
    ["m", "image/jpeg"],
    ["x", "8a2f04c1…"],
    ["x", "61bb90d3…"],
    ["t", "photography"],
    ["t", "costarica"],
    ["location", "Santa Teresa, Puntarenas, Costa Rica"],
    ["g", "d1u0q"]
  ],
  "sig": "90ab44cd12…"
}

A picture-first client builds its feed with one subscription. It asks for pictures and short videos together:

Session transcript - building the feed
→ ["REQ", "picfeed", {"kinds":[20,22], "limit":30}]
← ["EVENT", "picfeed", {"kind":20, "tags":[["title","Costa Rica coast"], …], …}]
← ["EVENT", "picfeed", {"kind":22, …}]  // NIP-71 short video, same feed
← ["EOSE", "picfeed"]
-- only JPEG-capable? filter by media type instead --
→ ["REQ", "jpegs", {"kinds":[20], "#m":["image/jpeg"], "limit":30}]
-- find every post that contains one exact image file --
→ ["REQ", "byhash", {"kinds":[20], "#x":["8a2f04c1…"]}]

6 Version History

NIP-68 arrived in late 2024 and stabilized within weeks. Notable changes, newest first:

  • 20262026-04Documented the optional thumbhash placeholder alongside blurhash.
  • 20252025-01Video companion updated: NIP-71 made video events regular, and the same-feed reference became kind 22.
  • 20242024-11Added the x tag so posts are indexable by image sha256; simplified the text.
  • 20242024-10Added location and geohash tags, and L/l language labels for text written in images.
  • 20242024-10Restricted posts to the six accepted media types.
  • Origin2024-10-27Vitor Pamplona proposed kind 20 under the working title "Instagram feeds".

Original Proposal - Credits

AuthorVitor Pamplona
First commit2024-10-27 — “Instagram feeds”
Statusdraft - optional — only picture-first clients need it
Source68.md on GitHub - full change history

7 See Also

  • NIP-01 — the base event format, kinds, and the p tag used here.
  • NIP-92 — defines the imeta tag that carries all per-image metadata.
  • NIP-94 — file metadata; the source of the x sha256 hash convention.
  • NIP-71 — video events; kind 22 short videos share the picture feed.
  • NIP-36 — sensitive content; the content-warning tag used for NSFW posts.
  • NIP-32 — labeling; the L/l namespace used for image languages.