{"id":"d9b857dd23a1b1b718569835cc6a768ba5c4cb3d35e186008fbe3b3098d66eab","pubkey":"af70ff513cc8f7428085fb8415f6ba402a5fbc1cab6382caa65530338ef07bdd","created_at":1786387906,"kind":30817,"tags":[["d","timeline-cards"],["title","Timeline Cards"],["alt","Nostr Implementation Possibility: Timeline Cards"],["client","NostrHub"]],"content":"Timeline Cards\n--------------\n\n`draft` `optional`\n\nA timeline card is an addressable event placing one dated fact on a shared timeline.\nCards carrying the same collection label form a collection; any key may publish into\nany collection, and no key may exclude another. Discovery is a marker in the indexed\n`t` tag, plus query shadows: self-labels for date and jurisdiction, a `g` tag for\ngeohash.\n\nThe key words \"MUST\", \"MUST NOT\", \"REQUIRED\", \"SHALL\", \"SHALL NOT\", \"SHOULD\",\n\"SHOULD NOT\", \"RECOMMENDED\", \"MAY\", and \"OPTIONAL\" in this document are to be\ninterpreted as described in RFC 2119.\n\n## Motivation\n\nTwo problems recur wherever events are recorded permissionlessly.\n\n**An open discovery marker is squattable.** This NIP splits discovery from membership:\na marker for the relay to filter on, and a shape test the client applies to what comes\nback.\n\n**A client that does not implement a spec silently drops the tags it does not know**,\nso declared state fails to travel. This NIP declares no state a card's meaning depends\non: the load-bearing signal is a field's **presence or absence**, which no client can\ndrop, and a dropped marker (`fork`, `adopt`, `adapt`) leaves an ordinary card.\n\n## Event kind\n\nA timeline card is kind `30828`, an addressable event as defined in\n[NIP-01](01.md). The event is self-labeling per [NIP-32](32.md): every field is a tag\non the card itself. A client MUST ignore external kind `1985` labeling events when\napplying this NIP — a card's membership and axes are read from the card alone, so that\nno third party can attach a card to a collection its author did not choose.\n\n`content` is [djot](https://djot.net). Clients that do not implement djot SHOULD render\n`content` as plain text rather than as another markup language.\n\n### Required tags\n\n| Tag | Example | Meaning |\n|---|---|---|\n| `d` | `[\"d\",\"proposition-number-assigned\"]` | addressable identifier; republishing under the same `d` replaces |\n| `event_date` | `[\"event_date\",\"2026-07-01\"]` | the date the card is *about*, `YYYY-MM-DD` |\n| marker | `[\"t\",\"wikitimechain\"]` | discovery marker; see *Discovery* |\n| collection | `[\"L\",\"timeline.collection\"]` `[\"l\",\"example-timeline\",\"timeline.collection\"]` | which timeline this card belongs to |\n| date buckets | `[\"L\",\"timeline.date\"]` `[\"l\",\"2026\",\"timeline.date\"]` `[\"l\",\"2026-07\",\"timeline.date\"]` | query shadow of `event_date` |\n\n`event_date` MUST be a full `YYYY-MM-DD` date and is the card's position on the\ntimeline. It is distinct from the event's `created_at`, which records when this version\nwas signed, and from the OPTIONAL `published_at`; see *Optional tags*.\n\n**`d` is opaque.** How a publisher mints it is their business; a client MUST NOT\nderive meaning from it, whatever it appears to encode.\n\nA card MUST carry exactly one collection label. Its value is the collection's\nidentifier and SHOULD be lowercase kebab-case.\n\nBuckets exist because multi-letter tags are not indexed by relays. A card MUST emit a `YYYY` bucket, and MUST emit a `YYYY-MM` bucket\nwhen the month is known. **Buckets MUST encode only known precision.** Where the true\ndate is coarser than a day, the placeholder components of `event_date` MUST NOT be\nemitted as buckets — a card known only to the year emits no month bucket.\n\n### Location\n\nA card describing an event with a location SHOULD carry a jurisdiction ladder. If it\ndoes, the ladder MUST run from the top down to the event's true scope, with every rung\npresent.\n\n```json\n[\"L\", \"ISO-3166-1\"],            [\"l\", \"JP\", \"ISO-3166-1\"],\n[\"L\", \"ISO-3166-2\"],            [\"l\", \"JP-13\", \"ISO-3166-2\"],\n[\"L\", \"timeline.location\"],     [\"l\", \"jp-13-shibuya\", \"timeline.location\"]\n```\n\nRelays filter on a label's *value*, not on the (`L`,`l`) pair, so an omitted rung is not\nqueryable even when a deeper rung implies it. That is why the whole ladder is written\nout rather than derived.\n\n- ISO rungs MUST use ISO codes verbatim under the standard namespaces `ISO-3166-1` and\n  `ISO-3166-2`.\n- Rungs below the ISO-3166-2 level are namespaced `timeline.location` and SHOULD be\n  lowercase kebab-case, prefixed by their parent rung.\n- The ladder MUST NOT extend deeper than the event's real scope. A national act stops\n  at the country rung.\n\nA collection whose events have no location is valid and carries no ladder.\n\n### Optional tags\n\n| Tag | Example | Rule |\n|---|---|---|\n| `g` | `[\"g\",\"xn76urx6\"]` `[\"g\",\"xn76\"]` `[\"g\",\"xn7\"]` | geohash, point events only. Emitted as prefix rungs for proximity queries. MUST NOT be a jurisdiction's centroid. |\n| `t` | `[\"t\",\"taproot\"]` | freeform topic. Unspecified by design: no registry, no controlled vocabulary. |\n| `published_at` | `[\"published_at\",\"1784681375\"]` | original publication time as in [NIP-23](23.md). SHOULD be preserved across replacements of the same `d`, while `created_at` changes with each. |\n| `event_time` | `[\"event_time\",\"14:30\"]` | UTC time of day refining `event_date`. `HH:MM` or `HH:MM:SS`, 24-hour. MUST encode only known precision. Emits no bucket and MUST NOT affect discovery or dedup. |\n\n## Text fields\n\nThree fields carry a card's text, a gradient of length:\n\n| Field | Example | Role |\n|---|---|---|\n| `title` tag | `[\"title\",\"Proposition number assigned\"]` | the shortest — names the card |\n| `summary` tag | `[\"summary\",\"The vendor shipped patched firmware for every affected model.\"]` | the event in plain words, in the publisher's voice; SHOULD be one or two sentences |\n| `content` | djot body | the longest — the full statement, where links and sources live |\n\n**None is required.** A card SHOULD carry at least one; a card carrying none is valid\nand says nothing. When `summary` and `content` are both present, `summary` MUST NOT\nassert anything `content` does not support.\n\n**All three are human-readable free text.** A client MUST NOT parse any of them for\nmachine-readable state, and MUST NOT require any particular wording or form. The only\nmachine-readable signal these fields carry is **presence or absence**; see *Motivation*.\n\nWhat a given combination *means* — a `summary` standing alone, a bare `title` — is\nvocabulary, and vocabulary belongs to a corpus, not to this NIP. A publisher MAY build\nconventions on these shapes; a conforming client MAY surface a shape distinctly, but\nMUST derive it from presence and absence alone, never from wording. Declared states\nsuch as *corroborated* or *disputed* are deliberately absent: no publisher can credibly\nmake such a claim about their own card. Disagreement is expressed by a second key\npublishing the same `d`.\n\nA client MAY render every field a card carries, or use the gradient as tiers of\ndisclosure — `title` in a list, `summary` in a preview, `content` on open. A client\nrendering a subset MUST fall back to the fields the card does carry, so that no card\ncarrying any text renders blank.\n\nFilling a card in under its own `d` — adding the `content` a `summary` anticipated —\nis an ordinary replacement per [NIP-01](01.md), not a revision: no `fork` marker is\ninvolved, because the recorded event never changed. A publisher MUST NOT reuse a `d`\nfor an unrelated card: reactions and comments address `30828:<pubkey>:<d>` and would\nsilently reattach.\n\n## Revisions\n\nA card MAY carry a revision of a document that another card records. Such a card MUST\ntag its parent with a `fork` marker in both forms:\n\n```json\n[\"a\", \"30828:<pubkey>:<parent-d>\", \"\", \"fork\"],\n[\"e\", \"<event id of the version revised>\", \"\", \"fork\"]\n```\n\nThe third element is the relay-hint slot and MUST be present, empty if unknown; the\nmarker is the fourth element. Both tags are REQUIRED: the `a` coordinate survives an\nedit of the parent, while the `e` id pins the exact version this card was built\nagainst.\n\nThe change itself is expressed with djot's insert and delete syntax, `{-removed-}` and\n`{+added+}`. A client MUST NOT render a card as a diff unless it carries both a `fork`\nmarker and marks in `content`; either alone is an ordinary card.\n\nA marked card asserts two documents, and both MUST be recoverable from it: stripping the\ndeletions and unwrapping the insertions yields the newer document exactly, and the\nconverse yields the older. A publisher SHOULD verify a marked card by performing both\nreconstructions rather than by reading it.\n\n## Adoption and adaptation\n\nA card MAY cite another card as its source. Two markers, in the slot the `fork`\nmarker uses:\n\n- `adopt` — the card carries the source's content verbatim. A card MUST NOT\n  carry more than one `adopt`.\n- `adapt` — the card's content is its own, built on the source. A card MAY carry\n  many.\n\nEach source is cited with three tags, all REQUIRED, carrying the same marker:\n\n```json\n[\"a\", \"30828:<pubkey>:<source-d>\", \"\", \"adopt\"],\n[\"e\", \"<event id of the version carried>\", \"\", \"adopt\"],\n[\"p\", \"<pubkey of the source's author>\", \"\", \"adopt\"]\n```\n\nThe `a` coordinate survives replacement of the source, the `e` id pins the exact\nversion cited, and the `p` tag routes credit. The adopting card keeps its own\n`d`; a shared `d` under a different key remains the rival-version case, not\nadoption.\n\n**The `e` id is a commitment, not a link.** A client MUST NOT dereference it: no\nfetching the source, no version comparison, no derived \"since edited\" state.\n\nAn adopting card is an ordinary card. It MUST pass the membership gate on its\nown tags, and it deduplicates, replaces and displays like any other.\n\nA reaction ([NIP-25](25.md)) is not an adoption. Citing a source requires\npublishing a card.\n\nA client MAY render an attribution line from these tags (for example\n`via <npub> · adopted`), derived at read time.\n\n## Discovery\n\nA client discovers cards with a single indexed filter (the limit is illustrative):\n\n```json\n{\"kinds\": [30828], \"#t\": [\"<marker>\"], \"limit\": 500}\n```\n\nThe marker names a corpus, not this protocol. It is a lowercase single-word `t` value\nchosen by whoever starts one; `wikitimechain` is the marker of the first deployed\ncorpus. A card MUST carry the marker of the corpus it belongs to, and a client is\nconfigured with the marker or markers it reads. A marker is permanent in practice, since\nchanging it requires re-signing every card.\n\n**The marker and freeform topics share the `t` tag.** A card MAY therefore carry many\n`t` values, and nothing distinguishes a marker from a topic by inspection. Two\nconsequences, both intended:\n\n- A client MUST test for the *presence* of its corpus marker among a card's `t` values,\n  and MUST NOT assume a card carries only one.\n- A topic value on an unrelated card may coincide with a corpus marker, and that card\n  will be returned by the corpus's discovery filter. This is not a defect to be patched\n  in the tag layout; it is precisely what the membership gate below exists to absorb.\n\n### The membership gate\n\n**A marker is discovery bait, not proof.** Any key may wear any marker. A client MUST\napply the following test to every event it receives, whatever the source, and MUST\ndiscard events that fail it:\n\n> the marker, **and** a parseable `YYYY-MM-DD` `event_date`, **and** a\n> `timeline.collection` label.\n\nThe collection identifier is read from that label's value.\n\n### Labels are indexed by value only\n\nRelays index a label by its value; the namespace (the `L` tag, and the label's third\nelement) is **not** part of the filter. `{\"#l\":[\"2026\"]}` matches the value `2026` in\nany namespace, including an unrelated one.\n\nImplementations MUST NOT substitute an `#l` filter for the membership gate. Value\ngrammars in this NIP (ISO codes, kebab identifiers, `YYYY` and `YYYY-MM` dates) are\nkept mutually distinguishable to make collisions unlikely, but they are not a guarantee\nand MUST NOT be relied on as one.\n\n### Axes do not compose\n\nCollection, date and location are all `l` values. A single filter's `#l` array is an\nOR, and two constraints sharing the key `l` cannot be AND-ed in one `REQ`. A client\nrequiring more than one axis MUST fetch on the most selective one and filter the\nremainder locally. Implementations MUST NOT assume server-side multi-axis AND.\n\nExact membership of one collection is:\n\n```json\n{\"kinds\": [30828], \"#l\": [\"<collection>\"]}\n```\n\nwhose results MUST be re-checked against the gate.\n\n## Client behavior\n\n- Cards are deduplicated by the addressable-event rules of [NIP-01](01.md) — newest\n  `created_at` per (kind, pubkey, `d`) — which, since this NIP defines a single kind,\n  is a key of pubkey and `d`.\n- Cards from **different** pubkeys sharing a `d` MUST NOT be collapsed. They are rival\n  versions of one entry and a client SHOULD present them as such rather than choosing\n  between them silently.\n- A card's displayed date MUST be `event_date`. Date buckets are query shadows and MUST\n  NOT be used for display.\n- Inbound citations of a card are queried with `#a`, `#e` or `#p` filters, and the\n  results MUST be filtered to events carrying an `adopt` or `adapt` marker — a bare\n  `#p` match is an ordinary mention. Any reputation derived from these edges is\n  computed at read time; no score is stored or declared.\n\n### Marginalia is addressed by kind\n\nReactions ([NIP-25](25.md)) and comments ([NIP-22](22.md)) address a card at\n`30828:<pubkey>:<d>`. Republishing the same content under a different kind changes the\naddress and silently orphans every existing reaction and comment.\n\n## Publishing\n\nA publisher signs a kind `30828` with the required tags and sends it to any relay. A\ncollection exists once a card carries its identifier; there is no registry. What a\nclient chooses to show is outside this NIP.\n\n## Security considerations\n\n**The marker is squattable** (see *The membership gate*). The gate discards an unusable\ncard on shape; it cannot establish authorship or good faith, and it is not intended to.\n\n**There is no author allowlist.** Any key passing the gate is admitted, which is the\npoint of the scheme and also its cost: junk and forgeries pass a shape test as readily\nas records do. Clients expecting adversarial input SHOULD rank rather than gate, using\nsignals outside this NIP such as the reader's own follow graph. Reaction and citation counts MUST NOT be treated as\nauthority: keys are free to mint.\n\n**Citation edges are self-asserted.** A key may adopt its own cards under other keys;\ncopying without an `adopt` tag is undetectable. A cited `e` id\nmay reference a version no relay still holds; a dangling id is not an error.\n\n**Self-asserted values MUST NOT be treated as evidence.** `created_at`, `published_at`,\n`event_date` and `event_time` are all written by the signer and none is attested.\n\n## Example\n\nA region-scoped card, with no local rung and no geohash:\n\n```json\n{\n  \"kind\": 30828,\n  \"content\": \"Over the summer the elections authority assigns the measure its proposition number for the November ballot.\\n\\nSource: [Elections authority — ballot measures](https://example.org/ballot-measures)\",\n  \"tags\": [\n    [\"d\", \"proposition-number-assigned\"],\n    [\"title\", \"Proposition number assigned\"],\n    [\"published_at\", \"1784681375\"],\n    [\"event_date\", \"2026-07-01\"],\n    [\"event_time\", \"14:30\"],\n    [\"t\", \"wikitimechain\"],\n    [\"L\", \"timeline.collection\"],\n    [\"l\", \"example-timeline\", \"timeline.collection\"],\n    [\"L\", \"timeline.date\"],\n    [\"l\", \"2026\", \"timeline.date\"],\n    [\"l\", \"2026-07\", \"timeline.date\"],\n    [\"L\", \"ISO-3166-1\"],\n    [\"l\", \"JP\", \"ISO-3166-1\"],\n    [\"L\", \"ISO-3166-2\"],\n    [\"l\", \"JP-13\", \"ISO-3166-2\"]\n  ]\n}\n```","sig":"8983d2a749beeaad5165f152b92579fad3ffe56d9a17e70fe3fa3bcbf71df0bf451124ec9a4fb3f5df44f68e6bd29021f2aac6754e08a6c15c34126461c58d17"}