NIPs by PolleramaCommunity NIPs, surfaced by trustConnect
npub19yw8tkfh530...

NIP-XXXX — IPFS Archive Attestations

Published Jun 7, 2026
kind 7503

draft optional

Status: proposed draft. The kind number is a placeholder pending assignment via the nostr-protocol/nips repository. The reference implementation (Lodestar) uses 7503, an unused number in the regular range chosen only to avoid collisions — the digits carry no meaning. What is deliberate is the range: see [Why a regular kind](#why-a-regular-kind).

Abstract

This NIP defines a regular event kind that attests to a content-addressed archive of a Nostr identity's events and referenced media. The attestation is a signed commitment to a deterministic bundle (its manifest hash and IPFS root CID), making the archive verifiable — authentic, intact, and vouched-for by the pubkey — independently of any relay or media host, and offline.

Motivation

A pubkey's events are scattered across relays that can disappear, and its media lives on hosts that rot. A plain backup of those events and blobs is useful but untrusted: a recipient can't tell whether it has been altered or truncated. Two properties of Nostr let us do better:

  1. Every event is self-verifying (id = sha256(serialization), signed). An

archive can preserve this so each event proves its own authenticity.

  1. The author holds a key. The author can therefore *sign a commitment to the

whole bundle*, vouching for a specific snapshot at a known time.

This NIP standardizes that commitment so any client can produce and verify such archives interoperably.

Terminology

  • Bundle — a directory of files: manifest.json, events.jsonl, and

media/<sha256> blobs. It is content-addressed with UnixFS to a single root CID.

  • Manifestmanifest.json, a canonical JSON document committing to the

exact event set, media set, and crawl coverage.

  • Attestation — the event defined here, committing to the manifest hash and

root CID.

  • Canonical JSON — JSON with object keys sorted lexicographically at every

depth, no insignificant whitespace, and undefined/absent members omitted. Array element order is significant and fixed by the producer.

The attestation event

A regular event. Authored (signed) by the pubkey being archived.

{
  "kind": 7503,
  "pubkey": "<hex>",
  "created_at": 1730000000,
  "content": "{\"events\":1240,\"media\":318,\"blobs\":270,\"generator\":\"lodestar/1\"}",
  "tags": [
    ["manifest", "<sha256-hex of canonical manifest.json>"],
    ["cid", "<UnixFS root CID, CIDv1, base32 'bafy…'>"],
    ["event_count", "1240"],
    ["media_count", "318"],
    ["blob_count", "270"],
    ["oldest", "1609459200"],
    ["newest", "1729999000"],
    ["version", "1"],
    ["alt", "Archive attestation: 1240 events, 270 media blobs."]
  ]
}

Tags

tagvaluerequired
manifestsha256 (hex) of the canonical manifest.jsonyes
cidUnixFS root CID of the bundleyes
versionarchive format versionyes
event_countnumber of events committed torecommended
media_countnumber of media references recordedrecommended
blob_countnumber of media blobs embedded (bytes captured)recommended
oldestunix seconds of the oldest archived eventoptional
newestunix seconds of the newest archived eventoptional
altNIP-31 human-readable descriptionyes

The manifest and cid tags are the only values required for verification; the counts and oldest/newest are conveniences (they appear in the manifest too). content is an informational JSON summary and is not part of the verifiable commitment.

A pubkey MAY publish many attestations over time; each is a permanent snapshot. Clients showing "the latest archive" SHOULD filter { kinds: [7503], authors: [pubkey] } and take the greatest created_at.

Why a regular kind

Regular events (1000–9999) are stored permanently by relays and are never replaced, which is exactly right for an append-only history of archive snapshots. An addressable kind (30000–39999) with a d tag would make "my latest archive" a single queryable coordinate, but each new attestation would overwrite the previous one — destroying the snapshot history. We therefore use a regular kind and query the set by authors + kinds.

The bundle

manifest.json (canonical JSON)

{
  "version": "1",
  "type": "lodestar-archive",
  "generator": "lodestar/1",
  "pubkey": "<hex>",
  "npub": "npub1…",
  "createdAt": 1730000000,
  "eventCount": 1240,
  "eventIds": ["00ab…", "00cd…"],        // ALL event ids, sorted ascending
  "kindCounts": { "1": 1100, "0": 1, "1063": 80 },
  "oldest": 1609459200,
  "newest": 1729999000,
  "media": [ /* MediaItem records, sorted by (eventId, url, source) */ ],
  "mediaBlobs": ["1f3a…", "9b22…"],      // sha256 of every embedded blob, sorted
  "coverage": { /* CoverageManifest, see below */ }
}

eventIds and mediaBlobs are the corpus commitments. Because an event's id is the sha256 of its serialization, the sorted id list pins the event set exactly: no event can be added, removed, or altered without changing the list, and the list is reproduced from events.jsonl.

A MediaItem records one reference and its capture outcome:

{
  "url": "https://…",
  "source": "imeta" | "nip94" | "blossom-path" | "content-url" | "profile-picture",
  "eventId": "<hex>",
  "expectedHash": "<sha256 if the event declared one>",
  "actualHash": "<sha256 of fetched bytes, if captured>",
  "mimeType": "image/jpeg",
  "size": 48213,
  "status": "verified" | "captured-unverified" | "mismatch" | "cors-blocked" | "not-found" | "error",
  "verified": true | false | null,       // null = no declared hash to check against
  "servedFrom": "https://…"
}

verified: null is distinct from false: it means integrity was unprovable (the reference carried no signed hash), not that a check failed.

events.jsonl

Every archived event, one canonical-JSON event per line, sorted by event id. Each line is an unmodified, signed NIP-01 event.

media/<sha256>

Each successfully captured blob, named by the sha256 of its bytes (its content address). Blobs are deduplicated: identical bytes referenced by multiple events are stored once.

Coverage

{
  "pubkey": "<hex>",
  "crawledAt": 1730000000,
  "relaysQueried": ["wss://…"],
  "relays": [
    {
      "url": "wss://…",
      "status": "ok" | "error" | "partial" | "aborted",
      "eventCount": 1200,
      "pages": 4,
      "oldest": 1609459200,
      "newest": 1729999000,
      "reachedEnd": true,
      "hitWindowCap": false,
      "kindCounts": { "1": 1100 },
      "error": "optional message"
    }
  ],
  "totalUnique": 1240,
  "kindCounts": { "1": 1100 },
  "complete": false,                      // ALWAYS false — see Limitations
  "notes": ["…caveats…"]
}

Content addressing

The bundle is encoded as a UnixFS directory over manifest.json, events.jsonl, and the media/ blobs, using the **unixfs-v1-2025 import profile: CIDv1, raw leaves, sha2-256, 1 MiB fixed-size chunks, 1024 links per file node, 256-fanout HAMT directory sharding. The serialized form is a CAR** whose single root is the cid value; importing the CAR into any IPFS node reproduces that CID. Re-deriving the CID from the loose files (rather than importing the CAR) reproduces it only under the same profile.

The attestation deliberately commits to the bundle but is not contained in it — a signature cannot sign over bytes that include itself. The attestation is distributed alongside the bundle (e.g. as a sidecar attestation.json, or simply published to relays).

Verification

Given the bundle, the attestation, and the expected pubkey:

  1. Attestation — verify the attestation's signature and that pubkey equals

the archive pubkey and kind is this kind.

  1. Manifest — recompute sha256 over the exact manifest.json bytes; it MUST

equal the manifest tag.

  1. CID — re-pack the bundle files (or import the CAR) under the

unixfs-v1-2025 profile; the root CID MUST equal the cid tag.

  1. Events — for each line in events.jsonl, verify id == sha256(serialization)

and the signature; the set of ids MUST equal manifest.eventIds; every event's pubkey MUST equal the archive pubkey.

  1. Media — each media/<sha256> blob's bytes MUST hash to its filename; the

set MUST equal manifest.mediaBlobs.

An archive is verified only if every step passes. A verifier MUST NOT report success when a step was skipped (e.g. the CID was not recomputed); it should distinguish "verified" from "verified except CID."

Relationship to other NIPs

  • NIP-01 — events are stored and verified verbatim; ids and signatures are

the base of authenticity.

  • NIP-65 — the crawl SHOULD read the author's relay list (kind 10002) to

choose relays; the coverage manifest records which were used.

  • NIP-92 / NIP-94 / NIP-96 / Blossom — sources for media references and their

declared sha256 (x/ox, or the hash embedded in a Blossom URL path), enabling per-blob verification.

  • NIP-09 — deletion requests (kind 5) are archived as ordinary events;

archival is content-preservation and does not apply deletions. Clients MAY record deletion status separately.

  • NIP-31 — the alt tag describes the event for clients that don't implement

this kind.

Limitations

  • Completeness is unprovable. A crawl cannot prove it observed every event a

pubkey ever signed (pruned, offline, or unknown relays; silent caps). coverage.complete is therefore permanently false, and coverage records exactly what was reached. An attestation proves authenticity of its contents, never exhaustiveness.

  • Received messages. An author-only crawl captures sent events; it does not

capture DMs received (NIP-04) or gift wraps addressed to the pubkey (NIP-59), which are authored elsewhere.

  • Replaceable history. Relays usually retain only the latest version of

replaceable/addressable events, so historical versions are typically unavailable.

  • Unverifiable media. References with no signed hash (bare content URLs,

profile pictures) can be captured but not cryptographically tied to the event (verified: null).

Security considerations

  • Signatures prove authorship, not truthfulness; an attestation vouches only that

this pubkey assembled this snapshot at this time.

  • Verification requires a secure context (Web Crypto sha256) and MUST recompute

hashes from raw bytes rather than trusting the manifest's self-reported values.

  • A verifier should treat verified: null media as "present but unattested,"

never as "intact."

Open questions

  • Should the CID profile be pinned by this NIP or negotiated per-archive (a

profile field in the manifest)?

  • An optional prev tag linking to the previous attestation id, to form an

explicit snapshot chain?

  • An optional pinning/availability hint (gateway URL, pinning-service receipt)

distinct from the CID commitment?

  • Multi-key / delegated archives (NIP-26-style) — out of scope for v1.