Nostrautica docs

NIP-XX — Encrypted Event Networking (Nostrautica Protocol)

draft optional

This NIP defines a protocol for privacy-preserving event networking on Nostr: encrypted attendee registration, an encrypted per-event directory, AI-assisted matchmaking through an organizer-chosen coordinator service, moderated pre-recorded talks, members-only event content, and MLS (Marmot) group chat with multi-device support.


1. Terms and actors

All hex values are lowercase. hex32 means /^[0-9a-f]{64}$/.

2. Wire version

PROTOCOL_VERSION = 2.

Custom kinds occupy the addressable block 3160031611 and the gift-wrap rumor block 2160021610.

3. Ordering, replaceability, and revisions

These rules give every reader — app and coordinator, fetch and stream paths alike — one deterministic answer for which replaceable event is current, and give every mutable submission an explicit, replay-safe order. They are global and normative.

3.1 Latest-event rule (all replaceable/addressable kinds)

For any two events with the same (kind, author, d): the event with the higher created_at wins; on a tie, the event with the lexicographically lowest id wins (the NIP-01 convention). Every reader — app and coordinator, fetch and stream paths — MUST apply exactly this rule, so two conforming implementations never disagree about which replaceable event is current.

3.2 Monotonic publishing

A publisher of a replaceable event MUST set created_at to max(now, previous_created_at_for_that_address + 1). This makes §3.1 deterministic in practice and removes same-second collisions between successive business updates.

3.3 Revisioned mutable submissions

Sender-mutable records carry an explicit application revision; sender-chosen timestamps are never the primary ordering key.

Clients maintain rev monotonically per (coordinate, kind) in their own storage and MUST bump it on every edit.

3.4 Admin command ordering and replay horizon

21604 admin commands carry two required fields:

On backfill/restore, an expired command is skipped — an old revoke or recompute can never re-execute after a database loss. Approve/revoke interleavings resolve deterministically per subject instead of by relay arrival order.

Coordinators MUST retain their rumor dedupe ledger (wrap id + rumor id) indefinitely rather than pruning it by age. A coordinator re-scans its inbox from since: 0 on install and on restore, so any dedupe entry aged out under a fixed time-to-live makes the corresponding rumor look unseen on the next rescan and lets it replay — and only 21604 admin commands carry an expires bound; join requests, submissions, and grants do not. Bounding the ledger safely again requires a durable, protocol-level generation or expiry model that covers every rumor kind, not admin commands alone. Only kind-scoped receipts with their own natural single-use expiry (such as consumed Marmot key packages) may be pruned by age.

3.5 Coordinator installation generation and detach

The 31600 coordinator tag is three-element: ["coordinator", <pubkey>, <gen>] where gen is a positive integer chosen by the organizer, strictly increasing across every attach/detach/re-attach of the event (persisted in the organizer's 30078 event-keys backup as coordinator_gen). The 21603 install grant carries the same gen. A malformed coordinator tag (a missing element, a non-hex pubkey, or a non-positive-integer gen) MUST be treated as no coordinator at all.

Coordinator rules:

3.6 Grant authority

21602 key grants are accepted only when the seal author is E_id or the coordinator named (with current generation) in the newest fetchable 31600. A grant whose seal author is a formerly assigned coordinator MUST be rejected once a newer config no longer names it. Missing config is always "retry later," never "accept."

3.7 Detach hygiene, record authority, and handover

Deleting custody on detach (§3.5) is the honest coordinator's obligation; a malicious coordinator may retain copies of E_inbox and the ECK it held before detach. Detach therefore has protocol consequences beyond the coordinator's own state:

4. Encryption model

Every layer reuses an audited primitive — NIP-44 v2 for confidentiality, AES-256-GCM for media — with no bespoke cryptography.

Layer Construction Used for
Inbound NIP-44 v2, sender → E_inbox pubkey, inside NIP-59 gift wrap 21600, 21601, 21608, 21609, 21610
Outbound member content NIP-44 v2 with the raw 32-byte ECK as conversation key (no ECDH) 31603, 31604, 31606, 31607, 31608 private, 31610
Per-recipient NIP-44 v2 ECDH, coordinator → recipient 31605, all grants/status rumors
Self-store NIP-44 v2 self-conversation-key (or signer nip44 to own pubkey) 31602, 30078
Media AES-256-GCM, fresh key + 12-byte IV per blob, whole-file Blossom blobs

5. Gift wraps

Per NIP-59: rumor (unsigned) → kind-13 seal (signed by the true author, NIP-44 to the recipient, empty tags) → kind-1059 wrap (one-time key, single p tag, created_at randomized up to 2 days into the past).

6. Kinds

6.1 Registry

Kind Name Signer Visibility d
31600 Event Networking Config E_id public event d
31601 Invite List E_id public (hash-hidden) event d
31602 Self profile / reuse library account NIP-44 self blinded
31603 Directory Entry coordinator (or E_id) ECK blinded
31604 Roster coordinator (or E_id) ECK event d
31605 Match List coordinator NIP-44 → recipient blinded
31606 Match Matrix coordinator ECK event d
31607 Members-only Post E_id ECK random stable
31608 Event Page E_id public + ECK private event d
31609 Event Theme E_id public event d
31610 Talk coordinator (or E_id) ECK blinded
31611 Coordinator Announcement coordinator public nostrautica:coordinator
Rumor Name Seal author → recipient
21600 Join Request account → E_inbox
21601 Profile Submission account → E_inbox
21602 Key Grant E_id or the currently assigned coordinator → attendee
21603 Coordinator Grant (install) E_id → coordinator
21604 Admin Command E_id → coordinator
21605 Organizer Grant E_id → co-organizer
21606 Coordinator Status coordinator → organizer, and optionally the affected attendee
21607 Chat Device Attestation account → coordinator
21608 Profile Correction account → E_inbox
21609 Talk Submission account → E_inbox
21610 Attendee Withdrawal account → E_inbox

Standard kinds used: 0, 1, 3, 5, 6, 13, 14, 1059, 10000, 10002, 10050, 10063, 24242, 30023, 30078, 31923/31924/31925, and Marmot's 30443/443/444/445.

31610 extends past the originally-reserved 3160031609 addressable block, and 21610 past the originally-reserved 2160021609 rumor block; both remain inside their respective standard Nostr ranges (parameterized-replaceable 30000–39999, ephemeral 20000–29999).

6.2 Addressable kind specifications

Every field below is exactly what packages/protocol/src/schemas.ts and config.ts accept. ? marks an optional field; "(default …)" marks a field the schema fills in when absent; everything else is required. .strict() schemas reject unknown fields; the rest silently drop them.

31600 — Event Networking Config

31601 — Invite List

31602 — Self Event Profile / Reuse Library

31603 — Directory Entry

31604 — Roster

31605 — Match List

31606 — Match Matrix

31607 — Members-only Event Post

31608 — Event Page (menu + layout)

31609 — Event Theme

31610 — Talk

31611 — Coordinator Announcement

6.3 Rumor kind specifications

Every rumor below travels only inside a NIP-59 gift wrap (§5); none is ever a signed, relay-visible event on its own.

21600 — Join Request

21601 — Profile Submission

21602 — Key Grant

21603 — Coordinator Grant (install)

21604 — Admin Command

21605 — Organizer Grant

21606 — Coordinator Status

21607 — Chat Device Attestation

21608 — Profile Correction

21609 — Talk Submission

21610 — Attendee Withdrawal

7. Invite codes

An invite code is a throwaway nsec, transported only in the URL fragment (#/e/:naddr/join?code=<nsec>).

8. Media descriptors

.strict() schema; unknown fields are rejected (a descriptor drives coordinator fetch and transcoding, so an unexpected field is a hard error, not a silently-ignored one).

{
  "kind": "intro" | "talk",
  "url": ["https://…", …],                 // https-only, ≥ 1 entry
  "x": "<sha256 hex of ciphertext>",
  "ox": "<sha256 hex of plaintext>",
  "size": "<int ≥ 1, ciphertext bytes>",
  "m": "<mime type>",
  "duration": "<seconds, number ≥ 0>",      // REQUIRED when m starts with audio/ or video/
  "encryption-algorithm": "aes-gcm",
  "decryption-key": "<base64, decodes to exactly 32 bytes>",
  "decryption-nonce": "<base64, decodes to exactly 12 bytes>"
}

9. Coordinator lifecycle, billing, and announcements

10. Group chat (Marmot/MLS) and multi-device

10.1 Model

10.2 Chat Device Attestation (21607)

Sealed by the account key to the coordinator (schema in §6.3). proof is a BIP-340 signature by the chat device key, required on op:"add", over:

sha256( utf8( JSON.stringify(
  ["nostrautica-chat-device-v2", <coordinate>, <account-pubkey>, <chat_pubkey>, <created_at>] ) ) )

The coordinator MUST verify the proof before binding a device to an account — an account cannot attest a key it does not control. Only attested device keys are chat members: the coordinator authorizes exactly the active attested device keys and nothing else — the attendee/organizer account pubkey is never an implicit chat identity. A local-key account is no exception; it mints and attests its own per-device chat key like any other account type (§10.1), and that attested device — not the raw account key — is what participates. op:"revoke" needs no proof (the account is evicting a key it already named; possession is irrelevant to that decision). Bindings are per (coordinate, account); a chat pubkey MUST NOT be bindable to two different accounts; rebinding it to the same account (e.g. re-add after revoke) mints a fresh binding. On account revocation from the event, all of that account's device leaves are removed from the MLS group.

10.3 Device profiles (kind 0 on chat relays)

So that other Marmot clients (e.g. White Noise) display a human name for each device member, every chat device publishes a kind 0 profile signed by the chat device key, to the event's chat relay set only (the Marmot relays — never the account's general relays):

{ "name": "<account display name>", "about": "<note pointing at the account's npub>", "picture": "<optional>" }

10.4 Event→group routing

11. Talks

Normative caps: at most 10 distinct talk_d per speaker per event; a talk carries exactly one of a media descriptor (whose kind must be "talk") or an external_url (+ external_kind:"youtube"|"video"); a published 31610 is republished under a new ECK on rotation, with the old-address copy NIP-09-deleted. External-URL talks (a YouTube link or a direct .mp4 hosted off-Blossom, for clips too large to upload) live inside the ECK-encrypted content — members-only — but are never fetched by the coordinator (the §8 media-fetch allowlist is Blossom-origin-only), so they are view-only: never transcribed, never fed into matching. process_for_matching (default false) gates whether a Blossom talk is transcribed and folded into matching at all; talks are not matched by default.

12. Deletions

The coordinator issues NIP-09 kind-5 with both ["a", "<kind>:<pubkey>:<d>"] and ["k","<kind>"] for: a revoked or withdrawn attendee's 31603, a 31606 on visibility downgrade, and obsolete 31610 addresses after rotation or rejection. Deletion is best-effort; the privacy model never depends on relays honoring it.

13. Security considerations

14. Constants appendix

Wire-normative bounds (packages/protocol/src/schemas.ts, crypto.ts, giftwrap.ts, config.ts, event-page.ts unless noted):

Coordinator operational defaults (packages/coordinator/src/coordinator.ts and neighboring modules — tunable per deployment, not part of interop compatibility):