# Nostr Agent Onboarding — every document (llms-full.txt) Snapshot 2026-10-10. Source site: https://npub1d70emggs6jzun5lhvqnfqsd9reqmaarn2qjf6q3r02gryyl4v8sqjn44xe.nsite.lol/ · index: https://npub1d70emggs6jzun5lhvqnfqsd9reqmaarn2qjf6q3r02gryyl4v8sqjn44xe.nsite.lol/llms.txt Each document below starts with a line `=== FILE: ===`. === FILE: https://npub1d70emggs6jzun5lhvqnfqsd9reqmaarn2qjf6q3r02gryyl4v8sqjn44xe.nsite.lol/start.md === # Nostr Agent Onboarding — start here > **You are an AI agent and someone pointed you at this site.** It is a > curated knowledge base for building on, designing for, and reasoning about > **Nostr**. Read this page first (≈5 minutes). It tells you what Nostr is, the > rules you must not break, and which document to read for the task in front > of you. Everything here is plain Markdown; every page has a `.md` twin. > > Snapshot: **2026-10-10** · 40 documents · served as an nsite (the > site is itself a set of signed Nostr events — see §7). ## 0. How to read this site | If you have… | Read | |---|---| | 5 minutes | This page. | | ~30 minutes / ~120 kB of context | [`/llms-core.txt`](https://npub1d70emggs6jzun5lhvqnfqsd9reqmaarn2qjf6q3r02gryyl4v8sqjn44xe.nsite.lol/llms-core.txt) — the five-part core synthesis + the design synthesis + the kind cheatsheet, concatenated. | | A specific task | The **task router** in §3, then only the documents it names. | | A large context window | [`/llms-full.txt`](https://npub1d70emggs6jzun5lhvqnfqsd9reqmaarn2qjf6q3r02gryyl4v8sqjn44xe.nsite.lol/llms-full.txt) — every document, concatenated (~510 kB). | | A crawler | [`/llms.txt`](https://npub1d70emggs6jzun5lhvqnfqsd9reqmaarn2qjf6q3r02gryyl4v8sqjn44xe.nsite.lol/llms.txt) (index), [`/index.json`](https://npub1d70emggs6jzun5lhvqnfqsd9reqmaarn2qjf6q3r02gryyl4v8sqjn44xe.nsite.lol/index.json) (machine-readable catalog), [`/sitemap.xml`](https://npub1d70emggs6jzun5lhvqnfqsd9reqmaarn2qjf6q3r02gryyl4v8sqjn44xe.nsite.lol/sitemap.xml). | Fetch the **`.md`** URLs, not the `.html` ones, whenever you can: they are the canonical text, with no navigation chrome. ## 1. Nostr in one paragraph Nostr is a description, not a system: **signed JSON events** plus **dumb, plural, interchangeable relays**. A user mints a secp256k1 keypair, signs events with it, and publishes them to any number of WebSocket servers (relays); anyone can verify an event no matter where they got it from. Signatures move authenticity into the data itself, which frees identity from platforms and storage from trust. The price: there is **no global view, no consistency, and no guarantees** — only routing heuristics, redundancy, and webs of trust. Everything good about Nostr (credible exit, rug-pull resistance, permissionless building) and everything hard about it (discovery, spam, key loss, sync) follows from that one trade. To build on Nostr is to accept the trade explicitly. The whole wire protocol: client → relay `EVENT`, `REQ` (filters on `ids`, `authors`, `kinds`, `since`/`until`, `limit`, `#`), `CLOSE`; relay → client `EVENT`, `OK`, `EOSE`, `CLOSED`, `NOTICE`, `AUTH`. Event = `{id, pubkey, created_at, kind, tags, content, sig}`. Extensions are **NIPs**, at . ## 2. Rules an agent must not break These are the failures that recur when software (and especially AI-written software) touches Nostr. Each links to where it is argued in full. 1. **Never ask for, handle, or store a user's secret key (`nsec`).** Sign through a signer: NIP-07 (browser extension), NIP-46 (remote "bunker"), NIP-55 (Android, e.g. Amber). A web client must never see an nsec. → [signer-login](https://npub1d70emggs6jzun5lhvqnfqsd9reqmaarn2qjf6q3r02gryyl4v8sqjn44xe.nsite.lol/docs/patterns/signer-login.md), [how to develop §keys](https://npub1d70emggs6jzun5lhvqnfqsd9reqmaarn2qjf6q3r02gryyl4v8sqjn44xe.nsite.lol/docs/core/03-how-to-develop.md) 2. **One purpose, one keypair.** A bot, service, or site gets its own freshly minted key. Never reuse a human's key for a service, and never publish kind 0 / 3 / 10002 (profile, follows, relay list) from an identity whose profile you don't own. Replaceable events overwrite: one wrong publish destroys someone's data. 3. **Default to regular events.** Use replaceable (0, 3, 10000–19999), addressable (30000–39999) or ephemeral (20000–29999) only when you can state in writing why discarding history is correct. → [kind cheatsheet](https://npub1d70emggs6jzun5lhvqnfqsd9reqmaarn2qjf6q3r02gryyl4v8sqjn44xe.nsite.lol/docs/design/kind-cheatsheet.md) 4. **Reuse before you invent.** Check the kind registry, `nak nip `, and probe live relays (`nak req -k -l 5 `) before allocating a kind. Inventing a duplicate kind gets you fragmentation and zero readers. → [NIPs kind registry](https://npub1d70emggs6jzun5lhvqnfqsd9reqmaarn2qjf6q3r02gryyl4v8sqjn44xe.nsite.lol/docs/sources/nips-readme-kinds.md) 5. **Tags are the data model; `content` is for humans.** Single-letter tags are relay-indexed and queryable. Don't put structured data as JSON in `content`. 6. **Never destroy data you didn't understand.** When updating a shared replaceable event (profile, follow list, mute list…), mutate the original — keep unknown tags and fields — instead of regenerating it from your model. 7. **Routing is correctness, not optimisation.** Read an author from their *outbox* relays (NIP-65 kind 10002 `write`), deliver mentions to recipients' *inbox* relays, DMs to their kind 10050 list. No hard-coded "big five" relay list in a general-purpose client. → [how it should work](https://npub1d70emggs6jzun5lhvqnfqsd9reqmaarn2qjf6q3r02gryyl4v8sqjn44xe.nsite.lol/docs/core/02-how-it-should-work.md) 8. **Every relay is unreliable and every claim is unverified.** Verify every signature (discard invalid ones), dedupe by id, distrust `created_at`, relay hints and single-relay answers. Sanitize all content. 9. **Signed public events are permanent.** Nostr is publicity technology — not private by default. Test against throwaway local relays (`nak serve`, a Khatru scaffold), not public ones; publish to the public network only what is meant to be public, forever. → [testing](https://npub1d70emggs6jzun5lhvqnfqsd9reqmaarn2qjf6q3r02gryyl4v8sqjn44xe.nsite.lol/docs/patterns/testing.md) 10. **"Relay said OK" is not done.** Verify outcomes by reading back from the network (and, for sites, by fetching the gateway). 11. **Lean into the paradigm.** If you find yourself wrapping Nostr in a REST API with server-held state and usernames, stop. Auth, identity, storage and inter-process messaging come with the protocol. 12. **Hand off what you don't render.** Use NIP-89 handlers (kind 31990) for unknown kinds and add a NIP-31 `alt` tag to every exotic event you emit. ## 3. Task router | Your task | Read, in this order | |---|---| | Understand Nostr from zero | [01 What Nostr is](https://npub1d70emggs6jzun5lhvqnfqsd9reqmaarn2qjf6q3r02gryyl4v8sqjn44xe.nsite.lol/docs/core/01-what-nostr-is.md) → [02 How it should work](https://npub1d70emggs6jzun5lhvqnfqsd9reqmaarn2qjf6q3r02gryyl4v8sqjn44xe.nsite.lol/docs/core/02-how-it-should-work.md) | | Build a client / app | [03 How to develop](https://npub1d70emggs6jzun5lhvqnfqsd9reqmaarn2qjf6q3r02gryyl4v8sqjn44xe.nsite.lol/docs/core/03-how-to-develop.md) → [signer-login](https://npub1d70emggs6jzun5lhvqnfqsd9reqmaarn2qjf6q3r02gryyl4v8sqjn44xe.nsite.lol/docs/patterns/signer-login.md) → [testing](https://npub1d70emggs6jzun5lhvqnfqsd9reqmaarn2qjf6q3r02gryyl4v8sqjn44xe.nsite.lol/docs/patterns/testing.md) | | Design events or a new kind | [04 How to design](https://npub1d70emggs6jzun5lhvqnfqsd9reqmaarn2qjf6q3r02gryyl4v8sqjn44xe.nsite.lol/docs/core/04-how-to-design.md) → [design synthesis](https://npub1d70emggs6jzun5lhvqnfqsd9reqmaarn2qjf6q3r02gryyl4v8sqjn44xe.nsite.lol/docs/design/design-synthesis.md) → [kind cheatsheet](https://npub1d70emggs6jzun5lhvqnfqsd9reqmaarn2qjf6q3r02gryyl4v8sqjn44xe.nsite.lol/docs/design/kind-cheatsheet.md) → [kind registry](https://npub1d70emggs6jzun5lhvqnfqsd9reqmaarn2qjf6q3r02gryyl4v8sqjn44xe.nsite.lol/docs/sources/nips-readme-kinds.md) | | Relay selection, outbox, spam | [02 How it should work](https://npub1d70emggs6jzun5lhvqnfqsd9reqmaarn2qjf6q3r02gryyl4v8sqjn44xe.nsite.lol/docs/core/02-how-it-should-work.md) → [outbox model](https://npub1d70emggs6jzun5lhvqnfqsd9reqmaarn2qjf6q3r02gryyl4v8sqjn44xe.nsite.lol/docs/sources/outbox-model.md) → [fiatjaf's vision](https://npub1d70emggs6jzun5lhvqnfqsd9reqmaarn2qjf6q3r02gryyl4v8sqjn44xe.nsite.lol/docs/sources/fiatjaf-vision.md) | | Implement login / signing | [signer-login](https://npub1d70emggs6jzun5lhvqnfqsd9reqmaarn2qjf6q3r02gryyl4v8sqjn44xe.nsite.lol/docs/patterns/signer-login.md) (NIP-07 / NIP-46 / NIP-55, the three-keys model, known-bad library code) | | Write Go | [Go baseline: `fiatjaf.com/nostr`](https://npub1d70emggs6jzun5lhvqnfqsd9reqmaarn2qjf6q3r02gryyl4v8sqjn44xe.nsite.lol/docs/patterns/go-client-baseline.md) — `nbd-wtf/go-nostr` is archived | | Write JS/TS | [03 How to develop §tooling](https://npub1d70emggs6jzun5lhvqnfqsd9reqmaarn2qjf6q3r02gryyl4v8sqjn44xe.nsite.lol/docs/core/03-how-to-develop.md) + [examples](https://npub1d70emggs6jzun5lhvqnfqsd9reqmaarn2qjf6q3r02gryyl4v8sqjn44xe.nsite.lol/examples/) | | Publish a static site (nsite) | [nsite publishing](https://npub1d70emggs6jzun5lhvqnfqsd9reqmaarn2qjf6q3r02gryyl4v8sqjn44xe.nsite.lol/docs/patterns/nsite-publishing.md) → [known issues §7](https://npub1d70emggs6jzun5lhvqnfqsd9reqmaarn2qjf6q3r02gryyl4v8sqjn44xe.nsite.lol/docs/design/known-issues.md) | | Store files / media | [Blossom](https://npub1d70emggs6jzun5lhvqnfqsd9reqmaarn2qjf6q3r02gryyl4v8sqjn44xe.nsite.lol/docs/sources/blossom.md) | | Ship an Android app | [Zapstore source](https://npub1d70emggs6jzun5lhvqnfqsd9reqmaarn2qjf6q3r02gryyl4v8sqjn44xe.nsite.lol/docs/sources/zapstore.md) → [Zapstore publishing](https://npub1d70emggs6jzun5lhvqnfqsd9reqmaarn2qjf6q3r02gryyl4v8sqjn44xe.nsite.lol/docs/patterns/zapstore-publishing.md) | | Git on Nostr (NIP-34 / GRASP) | [GRASP as structural layer](https://npub1d70emggs6jzun5lhvqnfqsd9reqmaarn2qjf6q3r02gryyl4v8sqjn44xe.nsite.lol/docs/patterns/grasp-as-structural-layer.md) → [going public](https://npub1d70emggs6jzun5lhvqnfqsd9reqmaarn2qjf6q3r02gryyl4v8sqjn44xe.nsite.lol/docs/patterns/going-public.md) | | Provable time (OpenTimestamps) | [Merkle OTS extension](https://npub1d70emggs6jzun5lhvqnfqsd9reqmaarn2qjf6q3r02gryyl4v8sqjn44xe.nsite.lol/docs/patterns/merkle-ots-extension.md) | | Nostr keys as network identity | [FIPS](https://npub1d70emggs6jzun5lhvqnfqsd9reqmaarn2qjf6q3r02gryyl4v8sqjn44xe.nsite.lol/docs/sources/fips.md) | | Write tests | [testing](https://npub1d70emggs6jzun5lhvqnfqsd9reqmaarn2qjf6q3r02gryyl4v8sqjn44xe.nsite.lol/docs/patterns/testing.md) | | Know what's current / open | [05 Current frontier](https://npub1d70emggs6jzun5lhvqnfqsd9reqmaarn2qjf6q3r02gryyl4v8sqjn44xe.nsite.lol/docs/core/05-current-frontier.md) | | Understand the people & disagreements | [voices: fiatjaf](https://npub1d70emggs6jzun5lhvqnfqsd9reqmaarn2qjf6q3r02gryyl4v8sqjn44xe.nsite.lol/docs/voices/fiatjaf/README.md), [No Solutions podcast](https://npub1d70emggs6jzun5lhvqnfqsd9reqmaarn2qjf6q3r02gryyl4v8sqjn44xe.nsite.lol/docs/voices/no-solutions/README.md), [Constant](https://npub1d70emggs6jzun5lhvqnfqsd9reqmaarn2qjf6q3r02gryyl4v8sqjn44xe.nsite.lol/docs/voices/constant/README.md) | | Read primary sources | [NIP-01 full text](https://npub1d70emggs6jzun5lhvqnfqsd9reqmaarn2qjf6q3r02gryyl4v8sqjn44xe.nsite.lol/docs/sources/nip-01.md), [Hodlbod's *Building Nostr*](https://npub1d70emggs6jzun5lhvqnfqsd9reqmaarn2qjf6q3r02gryyl4v8sqjn44xe.nsite.lol/docs/sources/hodlbod-book.md), [nostrdesign.org](https://npub1d70emggs6jzun5lhvqnfqsd9reqmaarn2qjf6q3r02gryyl4v8sqjn44xe.nsite.lol/docs/sources/nostr-design-principles.md) | ## 4. Live disagreements you should know about Nostr has no authority, and its principal voices disagree. Don't flatten these into one "correct" answer when advising a human — name the trade-off. - **What the invention is:** keys/signatures (Hodlbod) vs. clients talking to many independent relays (fiatjaf) vs. the whole paradigm — keys *and* events *and* relays, taught explicitly (Constant). - **Spam filtering:** relay-side, via filtered read relays (fiatjaf) vs. client-side web of trust (Martti Malmi). - **Onboarding:** hide keys and relays to reduce friction (much of the client scene) vs. teach the paradigm — "relays and keys are not UX issues, they ARE the UX" (Constant). - **The NIPs repo:** keep numbered specs (Hodlbod) vs. a bare kind registry plus competing prose guides (fiatjaf, "The end of NIPs"). - **DMs:** whether private messaging belongs on a publication protocol at all. ## 5. Caveats — read before trusting a detail - **Dated knowledge.** Facts were verified on the dates stated inside each document (mostly August–September 2026). Library versions, relay policies and NIP texts change; re-check anything load-bearing against and the live network. - **Workstation references.** Many documents were written for one developer's workstation and mention paths like `~/Documents/nostr-dev/…`, `~/Production Environment/…`, or local services on `localhost:8080/8081`. **Those do not exist for you.** Treat them as worked examples of a setup you can recreate (a local relay, a local Blossom server, `nak`), not as resources you can reach. Pages that contain such references carry a note. - **Curated, opinionated.** The synthesis takes positions (see §4) and says so. The `voices/` section is the primary-source evidence behind them. ## 6. Tools worth knowing by name - **`nak`** — the Nostr army knife (fiatjaf): keys, encode/decode, `req`, `event`, `serve` (throwaway relay), `bunker`, `blossom`, `nsite`, `nip `. - **Libraries** — Go: `fiatjaf.com/nostr` (incl. the `khatru` relay framework). JS/TS: `nostr-tools`, NDK, Applesauce. Rust: `rust-nostr`. Android: Quartz (Amethyst). - **References** — , , , . ## 7. Provenance Curated by **Constant** (Wouter Constant, techno-ethica.com, `npub1t6jxfqz9hv0lygn9thwndekuahwyxkgvycyscjrtauuw73gd5k7sqvksrw`) from two working knowledge bases: `nostr-dev` (the practitioner's layer — patterns, measured gotchas, vendored sources) and `Nostr-exp` (the synthesis — what Nostr is, how it should work, how to develop and design, the voices). Compiled with AI assistance. This site is an **nsite** (NIP-5A): a kind 15128 manifest signed by `npub1d70emggs6jzun5lhvqnfqsd9reqmaarn2qjf6q3r02gryyl4v8sqjn44xe`, listing every file by SHA-256, with the blobs on Blossom servers. You can verify that what you fetched is what was published: ```bash nak req -k 15128 -a 6f9f9da110d485c9d3f760269041a51e41bef47350249d02237a903213f561e0 --limit 1 wss://relay.nsite.lol \ | jq -r '.tags[] | select(.[0]=="path") | "\(.[2]) \(.[1])"' # then: sha256sum of any file you fetched must match its path tag ``` Text from upstream NIPs is MIT-licensed; quotations remain their authors'. === FILE: https://npub1d70emggs6jzun5lhvqnfqsd9reqmaarn2qjf6q3r02gryyl4v8sqjn44xe.nsite.lol/docs/core/00-overview.md === > **Nostr Agent Onboarding** · [start here](https://npub1d70emggs6jzun5lhvqnfqsd9reqmaarn2qjf6q3r02gryyl4v8sqjn44xe.nsite.lol/start.md) · [index](https://npub1d70emggs6jzun5lhvqnfqsd9reqmaarn2qjf6q3r02gryyl4v8sqjn44xe.nsite.lol/llms.txt) · source: `Nostr-exp/README.md` · snapshot 2026-10-10 > > Paths such as `~/Documents/…`, `~/Production Environment/…`, `repos/…` and services on `localhost` refer to the author's workstation and are **not available to you** — read them as worked examples of a setup you can recreate. # Nostr-exp — a Nostr knowledge environment A curated, opinionated description of **what Nostr is, how it should work, how to develop for it, and how to design on it** — synthesized from the primary voices of the protocol and kept honest about where they disagree. Built 2026-08-31 from four research streams: fiatjaf's complete web essays (21 annotated), fiatjaf's on-Nostr publications (24 articles + 425 notes sampled from his relays), Constant's public Nostr corpus (721 notes, 19 long-form articles, plus web presence), and the No Solutions podcast (37 episodes, 8 analyzed from full transcripts) — layered on top of the knowledge already codified in the sibling `~/Documents/nostr-dev` workspace (Hodlbod's *Building Nostr* read in full, NIP-01, the NIPs registry, the outbox literature, nostrdesign.org, the OpenSats report, Nostr Rising). ## Read this first: `core/` The synthesis, in reading order: | Doc | Question it answers | |---|---| | [`core/01-what-nostr-is.md`](https://npub1d70emggs6jzun5lhvqnfqsd9reqmaarn2qjf6q3r02gryyl4v8sqjn44xe.nsite.lol/docs/core/01-what-nostr-is.md) | What is Nostr? Events, kinds, relays, NIPs, what it is *not*, why not the alternatives. | | [`core/02-how-it-should-work.md`](https://npub1d70emggs6jzun5lhvqnfqsd9reqmaarn2qjf6q3r02gryyl4v8sqjn44xe.nsite.lol/docs/core/02-how-it-should-work.md) | The intended architecture: the routing problem, outbox and its limits, hints, spam, no-global, the local relay, the failure model. | | [`core/03-how-to-develop.md`](https://npub1d70emggs6jzun5lhvqnfqsd9reqmaarn2qjf6q3r02gryyl4v8sqjn44xe.nsite.lol/docs/core/03-how-to-develop.md) | The practitioner's layer: build pieces not platforms, the dev loop, keys and signing, fault tolerance, onboarding, anti-patterns, definition of done. | | [`core/04-how-to-design.md`](https://npub1d70emggs6jzun5lhvqnfqsd9reqmaarn2qjf6q3r02gryyl4v8sqjn44xe.nsite.lol/docs/core/04-how-to-design.md) | The design methodology: schema → class → routing → failure model, kind allocation, privacy, communities, value-for-value, trust, the arbitrating principles. | | [`core/05-current-frontier.md`](https://npub1d70emggs6jzun5lhvqnfqsd9reqmaarn2qjf6q3r02gryyl4v8sqjn44xe.nsite.lol/docs/core/05-current-frontier.md) | Where the live edge is (late 2026): relay feeds, Blossom/nsites/Zapstore, signers, TEPP, FIPS, the AI convergence, open problems. | ## The voices: `voices/` Primary-source research with distillations — including where the principals *disagree*, which is the most instructive part: - [`voices/fiatjaf/`](https://npub1d70emggs6jzun5lhvqnfqsd9reqmaarn2qjf6q3r02gryyl4v8sqjn44xe.nsite.lol/docs/voices/fiatjaf/README.md) — the creator. Relay-centric, anti-super-peer, "censorship-resistance is emergent client behavior." `essays.md` (web bibliography) + `on-nostr.md` (his relay publications). - [`voices/constant/`](https://npub1d70emggs6jzun5lhvqnfqsd9reqmaarn2qjf6q3r02gryyl4v8sqjn44xe.nsite.lol/docs/voices/constant/README.md) — Constant (Wouter Constant, techno-ethica.com), owner of this workspace: teach the paradigm, relays for curation, identity ≠ key, TEPP, OpenTimestamps. `research.md` is the full corpus pass. - [`voices/no-solutions/`](https://npub1d70emggs6jzun5lhvqnfqsd9reqmaarn2qjf6q3r02gryyl4v8sqjn44xe.nsite.lol/docs/voices/no-solutions/README.md) — Gigi & Pablo Fernandez's Sovereign Engineering podcast: no-global, rug-pull resistance, trade-offs out loud, the local relay, Nostr as AI substrate. `episodes.md` has the full episode table + transcript notes. Key live disagreements to know about: keys-vs-relays as "the point" (Hodlbod vs fiatjaf), spam filtering relay-side vs client-side (fiatjaf vs Malmi), teach-the-paradigm vs hide-the-complexity (Constant vs much of the client scene), whether the NIPs repo should exist (fiatjaf vs Hodlbod), and whether DMs belong on Nostr at all (Constant/hzrd149 vs Malmi). ## Reference: `sources/` Vendored and summarized source docs, seeded from `nostr-dev/docs/sources/`: NIP-01 (full text), the NIPs kind registry, the outbox model, Blossom, fiatjaf's "vision" essay, Hodlbod's book pointer, nostrdesign.org principles, the OpenSats client report, Nostr Rising episodes, video pointers. Machine-local paths inside these files (e.g. `repos/building-nostr/`) refer to the `~/Documents/nostr-dev` workspace, which also holds the tooling (nak, ngit, local relay/GRASP/Blossom stack) for putting any of this into practice. **`nostr-dev/docs/sources/` is canonical** — these are copies, last synced 2026-09-21, and it also carries sources this directory does not (`fips.md`, `zapstore.md`). Refresh there, then copy across; the refresh recipe is in `nostr-dev/docs/README.md`. Likewise, the *how* layer lives there: the `patterns/` directory (signer login, nsite publishing, the Go baseline, Zapstore publishing) and [`known-issues.md`](https://npub1d70emggs6jzun5lhvqnfqsd9reqmaarn2qjf6q3r02gryyl4v8sqjn44xe.nsite.lol/docs/design/known-issues.md) are where this synthesis turns into working code. ## The one-paragraph version Nostr is a description, not a system: signed JSON events plus dumb, plural, interchangeable relays. Signatures move authenticity into the data itself, which frees identity from platforms and storage from trust; the price is that there is no global view, no consistency, and no guarantees — only routing heuristics, redundancy, and webs of trust. Everything good about it (credible exit, rug-pull resistance, permissionless building) and everything hard about it (discovery, spam, key loss, sync) follows from that one trade. To work on Nostr is to accept the trade explicitly: build small interoperable pieces, route events deliberately, state your trade-offs out loud, and keep the barrier to entry for the next client low — because the number of independent clients, not the user count, is the protocol's real health metric. === FILE: https://npub1d70emggs6jzun5lhvqnfqsd9reqmaarn2qjf6q3r02gryyl4v8sqjn44xe.nsite.lol/docs/core/01-what-nostr-is.md === > **Nostr Agent Onboarding** · [start here](https://npub1d70emggs6jzun5lhvqnfqsd9reqmaarn2qjf6q3r02gryyl4v8sqjn44xe.nsite.lol/start.md) · [index](https://npub1d70emggs6jzun5lhvqnfqsd9reqmaarn2qjf6q3r02gryyl4v8sqjn44xe.nsite.lol/llms.txt) · source: `Nostr-exp/core/01-what-nostr-is.md` · snapshot 2026-10-10 # What Nostr is > Part 1 of the core synthesis. Read this first. Sources: NIP-01 > ([`../sources/nip-01.md`](https://npub1d70emggs6jzun5lhvqnfqsd9reqmaarn2qjf6q3r02gryyl4v8sqjn44xe.nsite.lol/docs/sources/nip-01.md)), Hodlbod's *Building Nostr* ([`../sources/hodlbod-book.md`](https://npub1d70emggs6jzun5lhvqnfqsd9reqmaarn2qjf6q3r02gryyl4v8sqjn44xe.nsite.lol/docs/sources/hodlbod-book.md)), > fiatjaf's essays ([`../voices/fiatjaf/`](https://npub1d70emggs6jzun5lhvqnfqsd9reqmaarn2qjf6q3r02gryyl4v8sqjn44xe.nsite.lol/docs/voices/fiatjaf/README.md)), and the NIPs registry > ([`../sources/nips-readme-kinds.md`](https://npub1d70emggs6jzun5lhvqnfqsd9reqmaarn2qjf6q3r02gryyl4v8sqjn44xe.nsite.lol/docs/sources/nips-readme-kinds.md)). ## The one-sentence version Nostr — *Notes and Other Stuff Transmitted by Relays* — is an open protocol in which **users sign JSON events with their own cryptographic key and publish them to any number of dumb, interchangeable servers called relays**. That is the whole protocol. Everything else is convention layered on top. ## The load-bearing move: signatures decouple authentication from storage On the legacy internet, knowing that "person X said Y" requires trusting the server that stores Y. Data that is not cryptographically signed is tightly coupled to custody: only the platform can attest to its authenticity, which is exactly what makes the platform indispensable — and exactly what lets it censor, surveil, and lock users in. Nostr's single architectural move is to break that coupling. A Nostr event carries its author's public key, a hash of its own contents (`id`), and a Schnorr signature over that hash. Anyone can verify it; nobody can forge it; and — crucially — **it does not matter where you got it from**. An event is equally valid coming from a relay, an email attachment, a USB stick, or a carrier pigeon. In Hodlbod's formulation: *digital signatures decouple data storage from authentication.* Three consequences fall out, and together they are the protocol's entire value proposition: 1. **Identity is user-generated.** An identity is a secp256k1 keypair you mint yourself (`npub…`/`nsec…` in bech32 form). No registration, no issuer, no one who can revoke it. 2. **Relays are interchangeable mirrors, not authorities.** Any relay can drop you; none can silence you, because your followers can fetch the same signed events from any other relay — or from you directly. 3. **Clients are replaceable views.** Because data and identity live outside any application, users have *credible exit*: leave a client and you keep your identity, your data, and your whole social graph. Interoperability is not a feature of Nostr apps; it is the point of them. Note a live difference in emphasis between the principals here. Hodlbod's framing (above) is key-centric: signatures are the move, relays are downstream. fiatjaf inverts it: keys on their own are "meaningless" — "having a key is meaningless if you cannot publish messages to where the people you're trying to reach can read them" — and the actual invention is **clients talking to many independent, mutually-distrusting servers at once**. On his account the keys exist to make that multi-relay architecture safe, not the other way around. Both framings agree on every practical consequence; keep both in your head, because each catches mistakes the other misses (key-worship without reach; relay-craft without verifiable data). See [`../voices/fiatjaf/`](https://npub1d70emggs6jzun5lhvqnfqsd9reqmaarn2qjf6q3r02gryyl4v8sqjn44xe.nsite.lol/docs/voices/fiatjaf/README.md). ## The event: one data type for everything Everything on Nostr is an event: ```jsonc { "id": "", "pubkey": "", "created_at": 1682377261, // unix seconds, author-asserted "kind": 1, // 16-bit integer content type "tags": [["p", ""], ["e", "", ""]], "content": "usually human-readable text", "sig": "" } ``` Design decisions embedded in this shape, all deliberate: - **Kinds are numbers, not names.** In a system built on signed, immutable data, names can never be renamed — and names smuggle in meaning that varies by reader. Integers are meaningless, so *usage* determines meaning, the way natural language works. The registry of kinds (see [`../sources/nips-readme-kinds.md`](https://npub1d70emggs6jzun5lhvqnfqsd9reqmaarn2qjf6q3r02gryyl4v8sqjn44xe.nsite.lol/docs/sources/nips-readme-kinds.md)) is a dictionary written after the fact, not a schema enforced up front. - **Tags are the data model.** `content` is for humans; structured data goes in tags — a list of string-lists, ordered, with repeatable keys. Single-letter tags (`e`, `p`, `a`, `d`, `t`, …) are indexed by relays and therefore queryable; multi-letter tags are payload only. - **Timestamps are author-asserted and second-granularity.** Nostr punts on distributed time on purpose: there is no ordering authority, and pretending otherwise (with millisecond precision, say) would be a lie. If you need provable time, anchor externally (NIP-03 / OpenTimestamps); otherwise the social layer — reputation — handles dishonest clocks. - **The event id is a content hash**, which makes regular events referentially transparent: if you hold an id, the content can never change under you. ### Kind ranges: the storage contract The kind number also selects the relay's storage behavior — the one place where behavior and data got coupled (Hodlbod considers this a design wart, but it is load-bearing today): | Class | Range | Relay keeps | For | |---|---|---|---| | **Regular** | 1–2, 4–44, 1000–9999, 40000+ | everything | notes, replies, reactions — the default | | **Replaceable** | 0, 3, 10000–19999 | latest per `(pubkey, kind)` | profile, follow list, relay list | | **Ephemeral** | 20000–29999 | nothing | live signaling, auth, in-flight RPC | | **Addressable** | 30000–39999 | latest per `(pubkey, kind, d-tag)` | articles, repos, lists — replaceable with cardinality | Addressable events are referenced by *address* (`kind:pubkey:d-tag`, encoded as `naddr…`) instead of by id, because their content is expected to change. The trade-off is real: replaceability discards history and invites race conditions when two clients update the same event concurrently. **Default to regular** unless you can say in writing why throwing information away is correct. (More in [`04-how-to-design.md`](https://npub1d70emggs6jzun5lhvqnfqsd9reqmaarn2qjf6q3r02gryyl4v8sqjn44xe.nsite.lol/docs/core/04-how-to-design.md).) ## Relays: easy, dumb, and plural A relay is a WebSocket server that stores events and answers queries. The full protocol surface is a handful of verbs: - Client → relay: `EVENT` (publish), `REQ` (subscribe with filters), `CLOSE` - Relay → client: `EVENT` (deliver), `OK` (accept/reject), `EOSE` (end of stored events), `CLOSED`, `AUTH` (NIP-42 challenge) A filter matches on `ids`, `authors`, `kinds`, `since`/`until`, `limit`, and indexed tags (`#e`, `#p`, `#t`, …). That's the entire query language. Anything fancier — full-text search (NIP-50), counting, analytics — is a per-relay extension, not the protocol. Relays are "easy," not "simple" (Rich Hickey's distinction): WebSockets over HTTP over TCP is a boring, gate-kept stack — but it is *deployable by anyone this afternoon*, and that is the property that matters. Because the interface is minimal, there are hundreds of independent relay implementations and thousands of running relays, and none of them is special. What relays legitimately do beyond storage: - **Curation and access control** (via NIP-42 `AUTH`): a relay may serve one community, one topic, paying members, or a single user. This makes relays *services with intent*, not commodity infrastructure — and that is a feature. A relay with no distinctive value proposition has no business model, and unfunded infrastructure re-centralizes. - **Transport brokering**: because a relay is a publicly addressable mailbox, two programs can talk through one by exchanging (usually encrypted, often ephemeral) events. This is how remote signers (NIP-46), wallets (NIP-47), and data vending machines (NIP-90) work — services addressed by pubkey instead of IP address. ## NIPs: the protocol is descriptive, not prescriptive Protocol extensions are documented in NIPs — "Nostr Implementation Possibilities" — curated at . The governing norms: - **Implementation first.** A NIP documents what running code already does; the repo requires interoperating implementations before merging. Anyone can ship a new kind tomorrow without asking permission — adoption, not approval, decides what becomes "the protocol." - **Specs are written for humans.** Brief, readable, hackable — the opposite of standards-body prose. - **"There should be no more than one way of doing the same thing."** An ideal, not a law — but the burden of proof is on the person diverging. This is what *Building Nostr* calls **radical openness**: the protocol is defined by its implementations, standardization is discovery rather than invention, and the messiness (overloaded tags, race-prone replaceables, half-baked NIPs) is the accepted cost of a protocol no company can capture. Nostr is the JavaScript of protocols: hacked together, full of warts, alive. Even the NIP process itself is contested from within: fiatjaf's 2025 essay "The end of NIPs" proposes dissolving the repository into a bare kind-number registry plus competing prose guides, on the grounds that spec bureaucracy confers unearned "officialness," lets dead ideas linger, and — worst — trains developers to translate event schemas into UI while skipping the actually hard part, relay choice. Whether or not the repo survives, take the diagnosis seriously: the registry function (don't collide on kinds) is the essential part; the prose is commentary. ## What Nostr is not - **Not a blockchain.** No consensus, no global state, no token. Events are cheap, unordered, and duplicable. (Bitcoin appears only at the payments layer, as zaps — see [`04-how-to-design.md`](https://npub1d70emggs6jzun5lhvqnfqsd9reqmaarn2qjf6q3r02gryyl4v8sqjn44xe.nsite.lol/docs/core/04-how-to-design.md) §value-for-value.) - **Not peer-to-peer.** Deliberately client-server, because P2P fights the grain of the modern internet. P2P transports can be added as progressive enhancement; relays are the reliable baseline. - **Not private by default.** Nostr is **publicity technology**. A signed public event is a permanent, attributable, analyzable record; signatures *remove deniability*. Privacy is opt-in per use case: NIP-44 encryption, NIP-59 gift wrap, NIP-17 DMs, closed relays. Constant's blunter version: "Nostr is a protocol for PUBLICations, it is right there in the word" — a structurally bad fit for DMs and "encrypted privacy" things in general. Design with this in mind and say it out loud in anything you build. - **Not consistent.** In CAP terms Nostr chooses availability and partition tolerance, always. There is no "the network" to have a view of — only the relays you happened to ask. Any design that assumes a global view is fighting the protocol and will lose. ## Why not the alternatives One paragraph each, from *Building Nostr*'s appendix; the pattern to notice is that every alternative gives up one of the two essentials — user-held keys or storage-independent data: - **ActivityPub / Matrix (Mastodon et al.)**: federation of *home servers* that own identity and storage. Your admin can deplatform you and your data is unsigned, so there is no credible exit — the centralized model replicated at smaller scale, with less accountable admins. - **Secure Scuttlebutt**: the spiritual ancestor — cryptographic identity, dumb pubs — but events form per-device hash chains, so you must sync a whole feed to validate one message, one key can't be used from two devices, and partial replication is impossible. - **Pubky**: cryptographic identity + DHT-based discovery (genuinely better bootstrapping than Nostr's), but content lives unsigned on a single home server. Right identity, wrong storage. - **Bluesky / atproto**: signed data and a sophisticated architecture, but the insistence on a global network view requires firehose relays and app views so expensive that only Bluesky-the-company runs them. Centralization by capital cost. Nostr is the only design in this space with **both** user-held cryptographic identity **and** decentralized, redundant, signed storage. It buys this with inconsistency, jank, and unsolved problems (key rotation above all). That trade is the protocol. ## Vocabulary quick reference | Term | Meaning | |---|---| | event | signed JSON object; the universal data unit | | kind | integer content type; selects semantics + storage class | | relay | WebSocket event repository; interchangeable, plural | | client | any app that signs/reads events on a user's behalf | | NIP | a documented protocol convention | | npub / nsec | bech32 public / secret key | | nevent / naddr / nprofile | bech32 pointers (id / address / pubkey) with optional relay hints | | d-tag | user-chosen identifier that keys an addressable event | | outbox / inbox | relays a user writes to / accepts mentions & DMs at (NIP-65) — see [`02-how-it-should-work.md`](https://npub1d70emggs6jzun5lhvqnfqsd9reqmaarn2qjf6q3r02gryyl4v8sqjn44xe.nsite.lol/docs/core/02-how-it-should-work.md) | | zap | Bitcoin micropayment bound to an event (NIP-57 lightning, NIP-61 ecash) | | WoT | web of trust — follow-graph-derived reputation | === FILE: https://npub1d70emggs6jzun5lhvqnfqsd9reqmaarn2qjf6q3r02gryyl4v8sqjn44xe.nsite.lol/docs/core/02-how-it-should-work.md === > **Nostr Agent Onboarding** · [start here](https://npub1d70emggs6jzun5lhvqnfqsd9reqmaarn2qjf6q3r02gryyl4v8sqjn44xe.nsite.lol/start.md) · [index](https://npub1d70emggs6jzun5lhvqnfqsd9reqmaarn2qjf6q3r02gryyl4v8sqjn44xe.nsite.lol/llms.txt) · source: `Nostr-exp/core/02-how-it-should-work.md` · snapshot 2026-10-10 # How Nostr should work > Part 2 of the core synthesis: the *intended* network architecture — what > keeps Nostr decentralized when it is used correctly, and what quietly > re-centralizes it when it isn't. Sources: *Building Nostr* ch. 4, NIP-65, > fiatjaf's "A vision for content discovery" ([`../sources/fiatjaf-vision.md`](https://npub1d70emggs6jzun5lhvqnfqsd9reqmaarn2qjf6q3r02gryyl4v8sqjn44xe.nsite.lol/docs/sources/fiatjaf-vision.md)), > the Outbox literature ([`../sources/outbox-model.md`](https://npub1d70emggs6jzun5lhvqnfqsd9reqmaarn2qjf6q3r02gryyl4v8sqjn44xe.nsite.lol/docs/sources/outbox-model.md)), OpenSats client report > ([`../sources/opensats-advancements.md`](https://npub1d70emggs6jzun5lhvqnfqsd9reqmaarn2qjf6q3r02gryyl4v8sqjn44xe.nsite.lol/docs/sources/opensats-advancements.md)). ## The routing problem is the real problem Signatures make events valid anywhere; they do not make events *findable* anywhere. Decentralization therefore stands or falls on the **routing problem**: 1. Where should a given event be **sent**? 2. Where should a given query be **asked**? Get this wrong in either of the two obvious ways and the network degrades: - **Naïve replication** ("blast everything to every relay"): every relay must store everything, small relays become impossible, and the network collapses into a few mega-relays. That is redundant vertical scaling, not decentralization. - **Fixed relay lists** ("just use these 5 popular relays"): the popular relays become de facto platforms. If they drop you, your followers cannot find you, and the censorship-resistance story is over. This is not hypothetical. fiatjaf's 2024 experiment ("Nostr is not decentralized nor censorship-resistant") published two identical notes — one to three popular relays, one only to his own relay, announced exactly where clients are supposed to look (his NIP-05 and NIP-65 lists). Engagement on the second was a fraction of the first, which "shouldn't have happened at all" if clients followed *people* rather than relays. His verdict: "Nostr today is indeed centralized" — and the failure lives in clients that hardcode relay sets, not in the protocol. Censorship-resistance is an **emergent property of clients doing discovery properly**, or it does not exist. The same essay's flip side ("Nostr is pro-censorship"): every relay is private property, free to reject anything for any reason; the network resists censorship precisely because censorship is everywhere permitted and nowhere decisive — *provided* readers can look elsewhere, which is the client's job to make real. The correct answer is **intelligent, per-event relay selection** — routing *heuristics*, agreed on by convention so that writers and readers can predict each other. Think of each heuristic as a database index: it connects a query someone will plausibly run with the place matching events are stored. Heuristics are additive — publish to every location a legitimate reader would look, and *only* those (access control is precisely the deliberate omission of a heuristic). ## The outbox model — the paradigm heuristic NIP-65: each user publishes a **kind 10002 relay list** declaring where they write. The two flags: | Flag | Name | Meaning | |---|---|---| | `write` | **OUTBOX** | where this user publishes their public content | | `read` | **INBOX** | where this user expects to receive mentions and DMs | The rules that fall out: - To read someone's posts → query 2–3 of their **outbox** relays. - To publish your own post → your **outbox** + the **inbox** of everyone you `p`-tagged. - To send a DM → recipient's **inbox** (NIP-17 uses a dedicated kind 10050 DM inbox), plus your own for the record. - Cold start / unknown user → indexer + general relays, then upgrade to their outbox once you've seen their 10002. With outbox, each author is sovereign over their own distribution: routing is declared by the *author*, not chosen by the platform. This is why the literature is unanimous that **outbox is no longer optional for a serious general-purpose client** (OpenSats report; Amethyst talks to ~1,000 relays from a phone; the point is a flat traffic profile across many small relays instead of ten hubs). **Gossip** (Mike Dilger) is the superset in practice: outbox plus every other discoverable hint — relay hints in `e`/`p`/`a` tags, `nprofile`/`nevent` URIs, NIP-05 metadata, observed author presence. Use every signal you can get. fiatjaf is emphatic that reducing discovery to NIP-65 alone is "too restrictive," and that dropping per-event relay hints (as Coracle once proposed) is "a catastrophic idea": hints are what keep community-relay posts, conference relays, topical relays, archive relays, and even a fully-banned "Alex Jones" — whose kind 10002 no indexer will carry — reachable via nothing but an nprofile on a business card. Resilience comes from *deliberately mixing many mechanisms*, manual user action included. ## Outbox is one heuristic, not the routing model The most common architectural mistake after "ignore routing entirely" is "pretend NIP-65 covers everything." It covers exactly one case: author-addressed public social content. Other cases have their own heuristics, and a NIP that defines a new kind **without defining its relay selection is an incomplete spec**: | Content | Route by | Mechanism | |---|---|---| | Public notes, articles, profiles | author | outbox (kind 10002) | | Mentions, replies | mentioned users | inbox (kind 10002 / 10050) | | DMs (NIP-17/44/59) | recipient | DM inbox only — *never* outbox | | Community / group content (NIP-29, NIP-72) | the community | the group's declared relay(s) only; leaking to outbox violates the access model | | Topical / curated feeds | topic | topic relays, relay-advertised interest (NIP-66 kind 30166 recommendations) | | Search / aggregate | nobody | indexer relays, an explicit trade of centralization for capability | | Zap receipts | zap *request* author | their outbox (the wallet acts on the sender's behalf) | | Git / repo events (NIP-34) | the repo | repo's declared GRASP relays, then author outbox | | Service RPC (NIP-46/47/90) | the service | the relay(s) the service advertises | Publishing means applying **all** applicable heuristics for the readers you intend, and none for the readers you don't. ## Bootstrapping, hints, and healing **Bootstrapping.** Kind 10002 tells you where a user's content lives — but where do the 10002s live? You have to start somewhere: hard-coded indexer/default relays in the client (swappable, and honest about being a compromise), purpose-built profile indexes, and eventually perhaps a DHT. Bootstrapping is a *trust* problem as much as a network problem: relay recommendations (NIP-66) are only as good as the web of trust you filter them through. **Relay hints.** Tags that reference events carry optional relay URLs (and pubkey hints, which are more durable — a pubkey leads to an outbox even after relays rot). Hints are the fallback when heuristics fail — and they have a second, adversarial function: a hint baked into a signed event cannot be stripped without breaking the signature, so even censorious relays end up advertising where the censored content lives. Hints force federation and let clients route around damage. **Migration.** Relay selections change; signed data makes the fix trivial in principle — copy the events — but somebody has to do it, and the rule for who is: **the person who wants the event findable in a place is responsible for putting it there.** Change your outbox → re-publish your content to the new relay, or your 10002 becomes a lie ("Alice said her notes are on relay B; they weren't"). Group moves relay → the admin syncs. New indexer → its operator scrapes. Negentropy (NIP-77 set reconciliation) makes this cheap. Most clients still don't do it; a complete client must. **Proxies and the super-peer curse.** Read/write proxies that do relay selection *for* the client break at the edges: NIP-42 AUTH deliberately cannot be proxied, and the client-side context that drives selection (group membership, user intent) never reaches the proxy. Multiplexers that carry explicit client-side selections in a wrapper protocol are fine; opaque "smart" proxies re-create the platform. fiatjaf generalizes this into the **super-peer curse**: any trusted machine that hands clients "massaged, sorted, filtered, ordered data" — Bluesky's app views, Farcaster hubs, p2panda's Aquadoggo, and the aggregator/proxy experiments inside Nostr itself — means the users behind it "will be controlled, censored, mislead and tricked." If that architecture came to dominate Nostr he "would immediately declare Nostr a failed experiment." The legitimate home for that power is the relay layer itself — curation relays, WoT relays, search relays, AI-feed relays — chosen by the user, never interposed between follower and followed. ## Relays are services, not infrastructure The healthy end-state is not ten thousand identical relays; it is a market of *differentiated* relays: community homes, paid indexes, curated topic feeds, DM mailboxes, archives, algorithm relays that answer a query with a recommendation feed. NIP-43/86 (membership lists, management API — the Coracle line of work) formalize this: a relay may state who it serves and what its policy is, and clients should honor that. Constant's framing sharpens the point: "The point of Nostr was never to get rid of servers; the point of Nostr is to be free again to leverage them for what they are good for… It frees servers, just as much as it did the users." Use relays for what servers are good at — **curation and ordering**: relay feeds as a first-class client feature (fiatjaf pushes the same thing — favorite a curated relay *as a feed*, don't add it to your read list), NIP-29 rooms, curated topic relays. "Dumb relay" refers only to the standardized query interface; within accept/store/serve, a relay is in full control and free to be as smart as it likes. Two corollaries: - **Expose relays to users.** Per-note relay selection, visible relay provenance, user-editable selections. A client that hides relays "for UX" is quietly re-centralizing; making relays *legible* is what makes the user sovereign. (Coracle's per-note relay selection is the reference.) - **Relays need business models.** A relay that is indistinguishable from its neighbors can only be a subsidized commodity, and subsidized commodity infrastructure is how we got the old internet. Distinct services can charge — and users choosing whom they do business with is the alignment mechanism. ## Spam is a routing problem too fiatjaf's mature position ("The only solution to Nostr spam," 2025): spam is solved at the **relay layer**, by construction, not by client-side cleverness. - Main feeds have no spam problem: you read people you follow, from relays you chose. - Inbox spam: query `#p` mentions **only from the user's declared read relays** — and choose read relays that actually filter (WoT-gated, NIP-05-gated, PoW, paid). Filtered public inbox relays should be client defaults so newcomers never see spam. - Other people's threads: replies are fetched from *the author's* read relays; a spam-flooded thread "is their fault for not picking 'read' relays correctly" — hygiene is per-user, socially correctable, and the incentive lands on the right person. - Why not client-side WoT/mutes as the primary defense: content must be downloaded before it can be filtered (at scale that's the bandwidth of all spam, and a 500-reply flood starves naive queries); mutes can't beat infinite fresh keys; and purely local filtering is a tragedy of the commons. Client-side trust scoring remains useful as a *secondary* layer — ranking, impersonation warnings, zap-feed dampening — not as the wall. ## No global view — and that's the design There is no "the network." Nostr is partition-tolerant to its core: you can fetch any subset of events from any subset of relays, and you will never know whether you have "all" of anything. Every alternative that promises a global view (Bluesky's firehose being the clearest case) pays for it with a centralized chokepoint. *Building Nostr* names the positive version of this **digital localism**: the network topology is allowed to align with the social graph. Clusters overlap relays; relays serve clusters; not all parts of the network need to be connected for each part to be whole. Discovery bootstraps from "many different hackish attempts" (fiatjaf) — replies you happen to see, hints, NIP-05 addresses, invite links — and that friction is not a bug to engineer away with a global index; it is what a captureless network feels like. The No Solutions podcast crew states the same law from the builder's side: global view counts, global usernames, guaranteed deletion, consistent group state — "everything else is a lie… if you want a global view you need to rebuild Bitcoin" (Gigi). Their predicted eternal-September failure mode: newcomers "building REST APIs that give you global feeds." Don't be the REST API. ## The local relay: partition tolerance as a feature The strongest positive expression of no-global (No Solutions eps. 02/09/13): **a relay is not an external dependency** — "a server truly is an external dependency: one source of truth, power over you. A relay is not, because it can literally be internal — on my phone, at home — completely exchangeable." So put one on the device: - **The flight-mode test**: an app backed by a local relay reads and writes offline, rebroadcasts in the background when connectivity returns, and its optimistic UI is *free* — the data is already signed; whether it has reached other relays yet is secondary. - The local event cache doubles as the honest personalization layer: "what's been trending on my WoT relay in the last 48 hours" beats any global algorithm, computed from data the user already chose to hold. - Marketing translation, for people who flinch at "censorship resistance": it's **100% uptime**. - Missing-but-needed tooling around this: rebroadcast daemons that keep your events on your *current* relay set, and Blossom link-healing from the user's server list — signed data makes both trivial in principle; almost nobody ships them yet. ## The failure model you must build for Concrete engineering consequences, non-negotiable for anything serious: - Events arrive **duplicated** → dedup by id; ingest must be idempotent. - Events arrive **late and out of order** → `created_at` is a claim, not an ordering; render defensively, use threads/references for causality. - Events **don't arrive** → absence of evidence is nothing: "I asked 3 relays and got no result" never proves nonexistence. - Relays **lie, throttle, vanish, and reject** → treat every relay response as best-effort; retry elsewhere; never let one relay's answer be final. - Replaceable events **race** → last-write-wins per relay can differ across relays; fetch from several, take newest, and preserve fields you don't understand when re-publishing (see [`03-how-to-develop.md`](https://npub1d70emggs6jzun5lhvqnfqsd9reqmaarn2qjf6q3r02gryyl4v8sqjn44xe.nsite.lol/docs/core/03-how-to-develop.md) §fault tolerance). - Any event may be **malformed or adversarial** → validate signatures always, sanitize content always, treat relay hints and media URLs as untrusted input. Design for eventual, partial, best-effort consistency and Nostr is pleasant. Design against it and every layer of your app fights you. ## The one-page summary 1. Routing heuristics, not replication, are what scale Nostr. 2. Outbox/inbox for social broadcast; every other kind needs its own declared heuristic; specs without routing are incomplete. 3. Whoever wants data findable somewhere is responsible for it being there — including after relay changes. 4. Relays are differentiated services with intent; make them visible to users and let them charge. 5. There is no global view. Build local-first, partition-tolerant, and paranoid about relay honesty. === FILE: https://npub1d70emggs6jzun5lhvqnfqsd9reqmaarn2qjf6q3r02gryyl4v8sqjn44xe.nsite.lol/docs/core/03-how-to-develop.md === > **Nostr Agent Onboarding** · [start here](https://npub1d70emggs6jzun5lhvqnfqsd9reqmaarn2qjf6q3r02gryyl4v8sqjn44xe.nsite.lol/start.md) · [index](https://npub1d70emggs6jzun5lhvqnfqsd9reqmaarn2qjf6q3r02gryyl4v8sqjn44xe.nsite.lol/llms.txt) · source: `Nostr-exp/core/03-how-to-develop.md` · snapshot 2026-10-10 > > Paths such as `~/Documents/…`, `~/Production Environment/…`, `repos/…` and services on `localhost` refer to the author's workstation and are **not available to you** — read them as worked examples of a setup you can recreate. # How to develop for Nostr > Part 3 of the core synthesis: the practitioner's layer — what to build, how > to build it, and the responsibilities that come with shipping. Sources: > *Building Nostr* chs. 2–5, nostrdesign.org, the NIPs repo norms, and the > working practices proven in the sibling `nostr-dev` workspace. ## Build pieces, not platforms The single most repeated piece of advice across fiatjaf, Hodlbod, and nostrdesign.org: **build a small thing that does one thing well.** A client that does everything is fighting three wars at once (routing, UX, moderation) and will lose to the composability of the ecosystem around it. Because every app shares the same identity and data layer, your single-purpose app gets the whole network's users for free — a long-form reader, a group chat, a git forge, a calendar, a marketplace, each speaking events other apps also speak. Corollaries: - You do not need permission, a token, a company, or a spec merge to ship. Sign events, publish them, document your kinds. Adoption is the approval process. - You do not need to implement "Nostr" — only the NIPs your use case touches. Scope discipline is a feature: for kinds you don't handle, hand off via NIP-89 application handlers instead of rendering nothing (see below). - Interoperability is your value proposition, not a compliance cost. The moment your events only make sense to your own client, you have rebuilt a silo with extra steps. - **kind:1 maximalism** (fiatjaf): microblogging is "the central plaza of Nostr" and should stay deliberately dumb — announce your "other stuff" (articles, recipes, repos, events) with plain kind:1 notes that *link out* to your dedicated microapp, rather than expecting every feed client to render your kind. Superapp-ification raises the barrier to entry for new clients, and the barrier to entry for clients is the real decentralization metric. "It's ok if Nostr ends up having just 2 recipe-sharing clients, but it must have dozens of microblogging clients." ### If you're building a business on Nostr fiatjaf's playbook ("How to do curation and businesses on Nostr"): resist the tempting version — a closed platform that reuses Nostr identities, siphons the open network, imprisons content behind an API, and runs a clever algorithm on top. Even when it succeeds it enshrines you as a platform, with a platform's ads, legal exposure, and capture dynamics. Instead: (1) ship an interoperable, ideally open-source niche **client** with documented kinds; (2) put the secret sauce — curation, validation, search, AI, moderation — in a **relay you run and charge for**, and make it your client's default. Your relay is your website; the social layer (follows, comments) still rides the outbox model, so users keep portable identities and graphs that don't depend on you — which is exactly why they can afford to trust you. "You don't own the network, you're just competing against other websites on a leveled playing field." One sober caveat for web apps: a web client is chained to a domain and its owner's server, so it "can't ever be trusted as an installed client can." Until content-addressed app distribution matures, encourage users to keep a second client configured — interchangeability is the mitigation. ## The development loop 1. **Model the events first.** Kind, class, tags, content shape — this is your schema and your API, and it outlives your app. Work through [`04-how-to-design.md`](https://npub1d70emggs6jzun5lhvqnfqsd9reqmaarn2qjf6q3r02gryyl4v8sqjn44xe.nsite.lol/docs/core/04-how-to-design.md) before writing code. 2. **Check the registry and the wild.** Read the NIPs kind table; probe live relays for the kinds you're considering (`nak req -k -l 5 ...`) — plenty of kinds are occupied but undocumented. Reuse before inventing. 3. **Prototype against a local relay.** A throwaway relay (nostr-rs-relay, a Khatru scaffold, or a container) gives you deterministic tests without spamming the public network. Publish to public relays only what is meant to be public — signed events are forever. 4. **Then test against the real network,** because the real network has spam, malformed events, dead relays, and 30-second-old data — and your handling of those *is* the product. Two acceptance checks from the No Solutions school: the **flight-mode test** (does it work offline against a local relay, syncing later?) and the **paradigm test** — if you found yourself wrapping Nostr in a REST API with server-held state, stop: "you have to lean into the Nostr paradigm to get all this stuff for free… building REST APIs around Nostr is breaking the coolest part" (Pablo Fernandez). Nostr done right is *less* to build: authentication, storage, identity, and inter-process communication come with the protocol. 5. **Document your kinds in your repo** (kind, class, tag schema, content shape, routing heuristic, why). If you want interop, open a NIP PR once two implementations exist. ### Tooling that carries this loop (Reference environment: the `~/Documents/nostr-dev` workspace, which has all of this installed and a live local relay/GRASP/Blossom stack.) - **`nak`** — the Nostr army knife: decode bech32, fetch/filter events, sign and publish, generate keys, read NIP texts (`nak nip 34`), run a throwaway relay (`nak serve`), act as a bunker (`nak bunker`), push blobs (`nak blossom`), publish a whole static site (`nak nsite`), sync two relays with negentropy (`nak sync`). The default instrument for one-shot inspection; learn it early. - **Libraries** — JS/TS: `nostr-tools` (low-level primitives), NDK (high-level, outbox built in), Applesauce (reactive); Rust: `rust-nostr`; **Go: [`fiatjaf.com/nostr`](https://pkg.go.dev/fiatjaf.com/nostr)** — the protocol author's own library and the whole stack in one module (events, relays, pool, an outbox-aware `sdk`, the `khatru` relay framework, `eventstore` backends, Blossom, GRASP, and ~40 NIP packages). Note `github.com/nbd-wtf/go-nostr`, which most Go examples on the internet still import, **was archived 2026-01-24** and its README points here. Use a maintained library for serialization, signing, and relay pooling — hand-rolling NIP-01 serialization is how invalid ids happen. - **Local relays** — nostr-rs-relay (turnkey, SQLite), Khatru (embeddable Go framework — the right base for policy experiments and specialty relays). - **Reference sites** (links checked 2026-09-21) — [nostrbook.dev](https://nostrbook.dev) (protocol detail), [undocumented.nostrkinds.info](https://undocumented.nostrkinds.info) (kinds found in the wild), [nostrapps.com](https://nostrapps.com) (ecosystem), [github.com/nostrability/nostrability](https://github.com/nostrability/nostrability) (interop issue tracker — it is a GitHub repo, there is no `nostrability.com`; `nostrapp.link` no longer resolves either). ## Onboarding: teach the paradigm, don't hide it The prevailing client instinct — hide keys, relays, and events behind "accounts" and "services" to reduce friction — has a serious dissent, argued hardest by Constant: **if Nostr fades into the background, you have rebuilt the platform world.** "If you give users in your app an 'account' because that is easier, they will end up making another 'account' in the next app… we hardly differentiated from where we came from." The counter-program: - Users need to know about **keys** (so they know they're free to use any app), **events** (so they know their posts outlive any server), and **relays** (so they can move freely) — and that the app is a tool, not a service. "Relays and keys are not ux issues, they ARE the ux." - The paradigm is "not hard, it is just new." The aha-moment works like a riddle — you get it after the fact — and a dumbed-down, borked explanation destroys it. Nobody was taught TCP/IP, but everybody was taught what email *is*; teach what Nostr is. - Practical touches: label sharing as "inside Nostr / outside Nostr"; treat key creation as a small ceremony, not a hidden implementation detail; progressive custody (local key → NIP-55/46 provider → bunker) rather than custodial "accounts." This doesn't excuse hostile UX — it sets the target: *low-friction paths to a correct mental model*, not low-friction paths around it. ## Keys and signing: never touch the nsec The private key is the user's whole identity, unrotatable today. The bar: - **Default to signers, not raw keys.** Integrate NIP-07 (browser extension), NIP-55 (Android intent / Amber), and above all **NIP-46 remote signers ("bunkers")** — Hodlbod's pick as the most promising, since they work from any device and keep the key in one guarded place. Support bunker URIs from day one. (Nuance from fiatjaf, 2026: for an *introductory native client*, generating and storing an nsec locally, then graduating the user to acting as a NIP-46/55 provider, is a reasonable onboarding flow — the real threats are web apps, malicious apps, and device compromise, and "Amber/NIP-55 defends against the first two." Web clients should be held to the strict never-see-the-nsec bar; they are structurally less trustworthy.) **Implementation:** don't improvise signer login. Follow [`~/Documents/nostr-dev/docs/patterns/signer-login.md`](https://npub1d70emggs6jzun5lhvqnfqsd9reqmaarn2qjf6q3r02gryyl4v8sqjn44xe.nsite.lol/docs/patterns/signer-login.md), and test with the Signer Login Tester nsite linked there. - Every event you sign on a user's behalf is a permanent public record — sign exactly what the user intended, nothing else, and show them what they're signing where feasible. - For services and bots: one purpose, one keypair, minted with `nak key generate`, stored outside the repo, documented (which npub, what it's for, where the secret lives). Never reuse a human's key for a service; never publish kinds 0/3/10002 from an identity you don't own the profile of. - Onboarding without custody theater: password-encrypted key backup (NIP-49), 2-of-2/multisig bunkers where real custody UX is needed. ## Fault tolerance: the reading and writing discipline **Reading** (from *Building Nostr* ch. 2 — "a light touch"): - Invalid signature → discard, no exceptions (NIP-59 unsigned rumors are the one structured exception). - Malformed-but-honest events → degrade gracefully; assume incompetence before hostility. Show fallbacks, not crashes. Aggressive schema validation turns recoverable errors into broken UX and hurts interop. - But **don't repair non-standard data into legitimacy** — tolerating a minority-broken format (bech32 where hex belongs, say) helps it hijack the kind. Fault tolerance is for readers; conventions are for writers. - Adversarial content is normal: sanitize HTML, cap resource use, treat relay hints and media URLs as untrusted, apply user-configurable policy (WoT, PoW, domain lists) before auto-fetching anything. **Writing — the prime directive: never destroy data you didn't understand.** The classic Nostr bug family is one client clobbering another's data in a shared replaceable event (relay hints in kind 3 content, muted words in kind 10000, encrypted fields dropped on update). The pattern that prevents it: parse the event into your model *keeping the original event attached*, and on save, mutate the original — preserving unknown tags and content — rather than regenerating from your model: ```javascript readProfile = e => ({...decodeJson(e.content), e}) writeProfile = ({e, ...p}) => ({kind: 0, content: encodeJson(p), tags: e.tags}) ``` **Routing** is part of correctness, not an optimization: implement the heuristics from [`02-how-it-should-work.md`](https://npub1d70emggs6jzun5lhvqnfqsd9reqmaarn2qjf6q3r02gryyl4v8sqjn44xe.nsite.lol/docs/core/02-how-it-should-work.md) for every kind you emit, dedup by id, treat every relay as unreliable, and re-publish user data when their relay selections change. ## Ship the ecosystem, not just the app - **NIP-89 handlers**: when you meet a kind you don't render, look up handler events (kind 31990, vetted through the user's WoT via 31989 recommendations) and offer to open the content in an app that does. Publish handlers for *your* kinds so others can hand off to you. Walled gardens vertically integrate; Nostr apps recommend each other — and are stronger for it. Pablo's framing of why this beats an app store: "you see the people doing the thing… the people interacting with the tool *is* the distribution of the tool." - **`alt` tags** (NIP-31) on every exotic event, so unknown-kind fallbacks at least say what the thing is. - **`client` tags** where users consent — ecosystem legibility. - **Open source it.** Reference implementations are how conventions actually propagate; undocumented-but-open beats documented-but-closed. On Nostr you can even host the repo itself on the protocol (NIP-34 / GRASP: git over relays, issues and patches as events). - **Backwards compatibility is a UX duty, not a dogma.** Enhance existing formats when you can do so without overloading them; when you must break, mint a new kind and evangelize, don't mutate the old one under people. Be prepared to defend divergence — and to write code for *other* projects to get your change adopted; nothing persuades like a PR. ## Anti-patterns (each observed repeatedly in the wild) - Inventing a kind that already exists because you dislike its NIP — you get fragmentation and zero readers. - Fixed hard-coded relay list in a general-purpose client — re-centralization in a trench coat. - JSON blobs in `content` for structured data that belongs in tags — opaque to relays and other clients. - Overloading one kind (or one shared list event) for multiple purposes — the data-loss bug factory above. - "Smart" relay proxies that select relays for the client — break AUTH, break groups, hide provenance. - Trusting `created_at`, trusting a single relay's answer, trusting a hint — everything is a claim until verified or corroborated. - Skipping the spam story — permissionless writing guarantees spam, so a client without one has a missing core feature, not missing polish. The primary mechanism is routing (filtered read relays — see [`02-how-it-should-work.md`](https://npub1d70emggs6jzun5lhvqnfqsd9reqmaarn2qjf6q3r02gryyl4v8sqjn44xe.nsite.lol/docs/core/02-how-it-should-work.md) §spam); client-side WoT/PoW/mutes are the secondary layer. - AI-generated apps that skip routing/handlers/WoT because they're invisible — they demo fine and quietly damage the network's decentralization (*Building Nostr*'s closing warning). ## Definition of done, Nostr edition Your thing is done when: 1. Its event kinds are documented (schema + class + routing) somewhere public. 2. It signs via NIP-07/46/55 and has never seen an nsec. 3. It reads via the correct heuristics and survives dead/lying/slow relays. 4. It writes to the correct heuristics and preserves data it doesn't own. 5. Another client can consume its events from the spec alone — and at least one does, or could. 6. It hands unknown kinds off via NIP-89 instead of dead-ending. 7. Its users can leave it without losing anything. **Credible exit from your own app is the final acceptance test.** === FILE: https://npub1d70emggs6jzun5lhvqnfqsd9reqmaarn2qjf6q3r02gryyl4v8sqjn44xe.nsite.lol/docs/core/04-how-to-design.md === > **Nostr Agent Onboarding** · [start here](https://npub1d70emggs6jzun5lhvqnfqsd9reqmaarn2qjf6q3r02gryyl4v8sqjn44xe.nsite.lol/start.md) · [index](https://npub1d70emggs6jzun5lhvqnfqsd9reqmaarn2qjf6q3r02gryyl4v8sqjn44xe.nsite.lol/llms.txt) · source: `Nostr-exp/core/04-how-to-design.md` · snapshot 2026-10-10 # How to design for Nostr > Part 4 of the core synthesis: the design methodology — events, kinds, tags, > communities, money, and trust, plus the principles that arbitrate every > trade-off. Sources: *Building Nostr* chs. 2, 6, 7; the nostr-dev > [`design-synthesis.md`](https://npub1d70emggs6jzun5lhvqnfqsd9reqmaarn2qjf6q3r02gryyl4v8sqjn44xe.nsite.lol/docs/design/design-synthesis.md) and kind cheatsheet; nostrdesign.org. ## Designing on Nostr = two artifacts You are never designing "a system." The runtime is whatever relays exist. You are designing: 1. **An event schema** — kind(s) + tag set + content shape, and 2. **A routing strategy** — which relays each kind goes to and is read from ([`02-how-it-should-work.md`](https://npub1d70emggs6jzun5lhvqnfqsd9reqmaarn2qjf6q3r02gryyl4v8sqjn44xe.nsite.lol/docs/core/02-how-it-should-work.md)). Every other decision is downstream. Take them in this order: ### Decision 1 — the schema - **Tags are your data model; content is for humans.** Structured data in tags (strings only, short, 2–3 positions); prose/markdown in content. JSON-in-content is legacy exception, not precedent. - **Prefer more kinds over overloaded kinds.** "No more than one way to do the same thing" cuts both ways: reuse the existing kind when your thing *is* the same thing; mint a *separate* kind when it differs even modestly (NIP-52 splits date-based and time-based calendar events — that's the taste to copy). Overloading is the root of the cross-client data-loss bug family. Similarly, resist packing many concerns into one event: contention over a shared event (kind 0, the shared lists) is where sync conflicts live. Hodlbod: redesigning profiles today, he'd split every field into its own event. - **Reuse tag conventions**: `e`/`p`/`a`/`d`/`t`/`r`/`alt`/`expiration`/ `subject`/`client`… — if your data is "a reference plus an annotation," it is an existing tag plus content, not a new kind. Know the three tag roles — data (display), filter (single-letter, relay-indexed), behavior (kind-independent semantics like `-`, `h`, `expiration`) — and when resolving overloaded tags let the kind's own spec win over generic meanings. - **Positional tags rot.** If a tag wants 4+ positions, split it into associative companion tags instead. - **The optional-becomes-mandatory test** (fiatjaf's razor — apply it to every extension you're tempted to ship): *if this became widespread, could a client ignore it and still show correct content?* If not, it is not optional, however innocent it looks — it will ratchet, via user pressure, into an obligation on every present and future client, raising the barrier to entry. That mechanism (his YAML-NIP-05 parable) is what killed NIP-26 delegation as a general scheme and grounds his case against kind:1 edits: a client that doesn't fetch edits "will be displaying false information," so edits, once widespread, are mandatory — "a centralizing force… a slippery slope that should not be accepted." Extensions that pass the test: things a non-implementing client can simply not show (reactions, zaps, annotations with graceful fallback). Extensions that fail: anything that changes the meaning of already-published content out from under simple readers. ### Decision 2 — the event class Default **regular**; regular events keep history, keep referential transparency (id = content hash), and degrade gracefully. Escalate only with a written justification of what you're discarding and why: ``` one-current-value-per-user? / \ yes no | | keyed by a d-tag? instantaneous signal? / \ / \ yes no yes no | | | | ADDRESSABLE REPLACEABLE EPHEMERAL REGULAR ← default 30000–39999 10000–19999 20000–29999 ``` Smell tests: a settings event that accumulates → replaceable. A "current location" stream → ephemeral. An edited blog post → addressable. An editable *comment* → wrong, comments are accountable: regular + NIP-09 tombstone. A draft → addressable. Remember what replaceability costs: history gone, race conditions in, and only a social (not technical) resolution when two writes collide. ### Decision 3 — routing per kind For **every** kind you define: who reads this, with what query, and which relays must therefore hold it? Write the heuristic into the spec. A kind without a routing heuristic is an incomplete design (the geocache/topic-tag lesson: events whose natural query has no relay-selection story end up on hard-coded relays — centralization — or unfindable). ### Decision 4 — the failure and abuse model Duplicates, gaps, reordering, lying relays, race-prone replaceables — plus: who can spam this? impersonate in it? what leaks? An event schema is not done until you can answer "what does a hostile pubkey do with this?" (spam → WoT/PoW/paid relays; impersonation → WoT validation, *never* NIP-05 alone — addresses locate users, they do not authenticate them) and "what does this reveal?" (Nostr is publicity technology; metadata is data). ## Kind allocation, concretely 1. Search the NIPs registry table (vendored: [`../sources/nips-readme-kinds.md`](https://npub1d70emggs6jzun5lhvqnfqsd9reqmaarn2qjf6q3r02gryyl4v8sqjn44xe.nsite.lol/docs/sources/nips-readme-kinds.md)). 2. `nak nip ` for the relevant NIPs; read them. 3. **Probe the live network** — `nak req -k -l 5` against big relays; undocumented-but-occupied kinds are common, and colliding with one hurts both parties. 4. Fit an existing kind if you honestly can; match its schema exactly. 5. Else pick a free number in the correct class range, document it in-repo, ship, and NIP-PR it once a second implementation exists. ## Designing privacy: encryption vs. relay policy Two mechanisms give confidentiality, with opposite trade-offs: - **Encryption** (NIP-44 payloads, NIP-59 gift wrap, NIP-17 DMs, MLS for large groups): trustless — no relay can leak what it can't read — but policy is frozen into the protocol; every client must implement the same rules, and mistakes are catastrophic and unpatchable. - **Relay-enforced access control** (NIP-42 AUTH + policy, the NIP-29 model): flexible, cheap, admin-evolvable — any policy a script can express — but the relay is trusted; "forward secrecy" by policy is a promise, not a guarantee. Choose by threat model, and be honest in your spec about which you chose. Gift-wrapped events deliberately hide kind, author, and timestamp from relays; remember that even then, *traffic* metadata (who talks to which relay, when) still leaks — say so in your privacy story. Two more honesty checks from the practitioners. Group encryption past roughly twenty members is theater — "you'll either have a mole or an asshole in there" (Gigi); don't pay heavy cryptography costs for an assurance the social layer can't deliver, and consider Martti Malmi's alternative: use Nostr as the *lookup substrate* (publish your Signal/SimpleX handles on your profile) rather than forcing every private medium onto relays. And deletion is make-believe everywhere, not just here: "computers are copying machines… all deletions in a networked world are pinky-promise deletions." NIP-09/62 are courtesy requests; design as if nothing can be unpublished, because nothing can. ## Designing communities *Building Nostr* ch. 7's taxonomy — five community shapes, each wanting a different architecture. Naming which one you're serving is half the design: | Shape | Nature | Architecture fit | |---|---|---| | **Social cluster** | emergent, open, graph-defined | broadcast social + outbox; no membership machinery at all | | **Group chat** | small, every-member-trusted, quadratic intimacy | encrypted (NIP-17 pairwise; MLS for scale); doesn't scale socially past trust, don't pretend it does | | **Discussion forum** | open, topic-defined, needs moderation not gatekeeping | relay-based groups (NIP-29) or moderation-as-user-preference; forkable by design — same group on two relays merges in the client | | **Owned community** | exists for/by a central figure (creator, company) | centralized control is a *feature*; NIP-29 relay owned by the owner; interop is the value-add | | **Commons** | member-owned, politically self-organized | the hard one: roles, gradated membership, partitions by identity/topic/content-type; design *with* the community, not for it | Lessons paid for in the wild: NIP-72's require-moderator-approval design killed its forums (moderation must be *added under load*, not a precondition for content to exist); moderation policy generally belongs to users and relays, not frozen into the event spec; missing data is not a bug when the user's own filters caused it — partition tolerance is also a social feature. And know which value applies where — Constant on NIP-29: a group "is a house, you are a guest, and the fact that you can be yeeted out is a feature, not a bug. In this context, censorship resistance does not make sense." Demanding broadcast-network values inside a community context is a category error in both directions. The generalization of all this is Constant's TEPP formula: Nostr is **"a permissionless system for the creation of permissioned environments."** Permission lists over the protocol's own primitives — people (npubs), places (relays), things (kinds/events) — enforced redundantly at signer, client, and relay, extensible through webs of trust. Child-safe internet (Kidstr) is the flagship case, but the pattern covers any curated, bounded, or supervised space you might need to design. ## Designing for value-for-value Money is a first-class design material on Nostr, and its semantics are patronage, not trade: digital content has ~zero marginal cost, so payment buys *future production* plus identity/belonging — leaderboards, boosts with messages, in-group signaling. Design for those dynamics, not for a store. - **NIP-57 lightning zaps**: public receipts tie payment to event/recipient. Trust caveats: the receipt is only as honest as the receiver's wallet, and zap-driven feeds must be WoT-filtered or sybils farm them. - **NIP-60/61 cashu nutzaps**: wallet lives in events (follows the user), pubkey-locked tokens verify without trusting the wallet; custodian risk moves to mints. "Uncle Jim" community mints/nodes mitigate. - The underexplored direction: paying *infrastructure* — relays, DVMs, library authors — in-band and per-use, so the commons isn't ad-funded. If your design creates load on someone's server, design the payment path too. ## Trust and reputation as design material There is no global truth on Nostr, so **reputation is always relative to a starting point**. Design rules: - Compute WoT from *this user's* follow graph outward; global scores (PageRank-style) are sybil bait. Garbage in, garbage out. - Weight attestations by strength: a follow is weak-but-real interest; a reaction emoji is nearly nothing. Don't over-read weak signals. - Explicit attestation schemes ("I trust X for Y") mostly fail — PGP proved incentives beat elegance; piggyback on actions users already take. - Trust is domain-specific: good for spam/impersonation filtering, not a character reference. - Supplements for cold-start: PoW (NIP-13), attestation services, payments — each gameable, useful in combination. - **Identity ≠ key** (Constant): "an npub is a mere pointer to some identity… identity consists of expression and relation." Key rotation has no accepted formal solution — and Constant argues it shouldn't get one: rigid formal schemes "turn against you the moment an attacker penetrates them." Design instead for **social recovery**: WoT attestations of a new key, expiring musig2 "association events" whose silence signals compromise. fiatjaf lands nearby from the relaxed side: identity, unlike money, "can be recovered — slowly and painfully," so many optional schemes beat any mandatory one. - **Timestamps as trust material**: `created_at` is a claim, but an OpenTimestamps proof (NIP-03; Constant's NIP-3B/3C fixes — stamp hash(id+sig), embed the proof) makes *backdating detectable*: "timestamps only give very weak indications of truth, but they are really good at catching certain lies." Post-compromise, a stamped history is what lets an identity prove which of its record predates the theft. Cheap to add, rarely regretted. ## The principles that arbitrate When two design options tie, these break the tie (nostrdesign.org guiding principles + the synthesis of everything above): 1. **User sovereignty over identity** — keys never leave the user; no design may require an intermediary that can impersonate. 2. **User sovereignty over relays** — never hard-code, never hide; relay selection is user-facing UI, not an implementation detail. 3. **Modularity over monoliths** — the smaller and sharper your piece, the more the ecosystem carries it. 4. **Filter, don't censor** — clients give users filtering tools (WoT, PoW, mute, relay choice) instead of deciding for them; moderation is relay- and user-policy, not client fiat. 5. **Interoperability over features** — a feature that breaks other clients' reading of shared data is a bug with good PR. 6. **Trust users, trust developers, keep specs simple** — write NIPs a human reads in five minutes; complexity is capture surface. 7. **Embrace compromise** — Nostr is "good enough" by design; perfect protocols with no users lose to messy protocols with them. Protocol work is politics in the Aristotelian sense: a community building the place it intends to inhabit. Participate accordingly. 8. **State your trade-offs out loud** — the No Solutions rule ("no solutions, only trade-offs"). Mints can rug you: that's the design, keep pocket change there. Zapstore dials decentralization down for credible exit, and says so. The characteristic newcomer failure is refusing to accept a trade-off — and thereby smuggling a central authority back in (Passkeys, home servers, global registries, "smart" proxies, DRM). A design document that names what it gives up is the mark of someone who understood the protocol. === FILE: https://npub1d70emggs6jzun5lhvqnfqsd9reqmaarn2qjf6q3r02gryyl4v8sqjn44xe.nsite.lol/docs/core/05-current-frontier.md === > **Nostr Agent Onboarding** · [start here](https://npub1d70emggs6jzun5lhvqnfqsd9reqmaarn2qjf6q3r02gryyl4v8sqjn44xe.nsite.lol/start.md) · [index](https://npub1d70emggs6jzun5lhvqnfqsd9reqmaarn2qjf6q3r02gryyl4v8sqjn44xe.nsite.lol/llms.txt) · source: `Nostr-exp/core/05-current-frontier.md` · snapshot 2026-10-10 > > Paths such as `~/Documents/…`, `~/Production Environment/…`, `repos/…` and services on `localhost` refer to the author's workstation and are **not available to you** — read them as worked examples of a setup you can recreate. # The current frontier (as of late 2026) > Part 5 of the core synthesis: where the live edge of Nostr development > actually is, drawn from the freshest material in this repo — fiatjaf's > 2026 notes ([`../voices/fiatjaf/on-nostr.md`](https://npub1d70emggs6jzun5lhvqnfqsd9reqmaarn2qjf6q3r02gryyl4v8sqjn44xe.nsite.lol/docs/voices/fiatjaf/on-nostr.md)), the 2026 No Solutions > episodes ([`../voices/no-solutions/episodes.md`](https://npub1d70emggs6jzun5lhvqnfqsd9reqmaarn2qjf6q3r02gryyl4v8sqjn44xe.nsite.lol/docs/voices/no-solutions/episodes.md)), and Constant's active > projects (`../voices/constant/research.md`). Everything here is moving; > re-verify before building on it. ## What the principals are actually pushing right now - **Relay feeds as a first-class client feature.** Both fiatjaf and Constant: a curated relay should be *favorited as a feed*, not added to your read list. Supported across a dozen clients (Jumble, Nostur, Yakihonne, Coracle, fiatjaf's own Hallway…). Relays with personality — communities, curation, algorithms — are the underused half of the protocol. - **Boring sub-protocols being dogfooded to reliability**: NIP-17 DMs ("I just wanted proof that NIP-17 could work reliably, and now I got it" — fiatjaf), NIP-29 relay-based groups ("you only give partial authority to a server, you always keep your right to migrate"), NIP-34/GRASP git (working, still UX-rough — "will we ever get a 'merge' button?"). - **The NIPs process itself is in question**: fiatjaf now maintains a bare registry-of-kinds and argues for competing prose guides over numbered specs. Watch where documentation actually accrues. - **Spam doctrine consolidating around filtered read-relays** (WoT-gated inbox relays like Pyramid's `/inbox`, NIP-05-gated, PoW, paid) with client-side WoT as a secondary layer — though Martti Malmi still runs WoT client-side on principle (trusting the relay operator is the cost he won't pay). Unresolved, productive disagreement. ## Media and app distribution - **Blossom everywhere**: content-addressed blobs, user-declared server lists (kind 10063), the relay design pattern applied to media. Known gap: self-healing links — clients that repair a 404 from the user's server list — are still mostly unimplemented; the No Solutions podcast itself 404'd when one Blossom server died. - **nsites**: whole static sites as Nostr events + Blossom blobs (Constant's techno-ethica.com renders entirely from relays). Now specified as **NIP-5A**: one replaceable **kind 15128** manifest per pubkey listing every file by sha256, named sites at 35128, immutable snapshots at 5128, and a 2026 addition — an aggregate hash that makes two manifests provably the same site, plus `a`/`A` lineage tags so anyone can pin and republish someone else's site under their own key. Credible exit for a website. fiatjaf's "subjective apps" sketch goes further: a web client addressed by content hash, releases adopted voluntarily through the social graph — his answer to "web apps can't be true user agents." *(How to actually ship one, with the failure modes: [`~/Documents/nostr-dev/docs/patterns/nsite-publishing.md`](https://npub1d70emggs6jzun5lhvqnfqsd9reqmaarn2qjf6q3r02gryyl4v8sqjn44xe.nsite.lol/docs/patterns/nsite-publishing.md).)* - **Zapstore**: permissionless app distribution with WoT instead of a blessed catalog; evolving from "user against the world" verification to **community catalogs** that do the trust heavy-lifting. The forcing function is real: OS-vendor sideloading crackdowns ("you're either a fully-KYC'd developer or you're a terrorist" — Franzap). - **Composable clients**: napplets (sandboxed single-file mini-apps whose host runtime mediates signing, relays, uploads, permissions — "you should be able to compose your clients"), Soapbox Tiles, fiatjaf's revived nostr-apps idea. Direction: Nostr expertise concentrates in runtimes, relays, and libraries; app authors stop needing to know about keys at all. ## Identity, trust, and time - **Signer maturity**: NIP-46 bunkers, Amber/NIP-55, FROST/musig2 threshold schemes ("pomegranate"); next frontier is signing *UX* — specialized signers, budgets, "the user must understand what they're signing within half a second, then you can auto-approve" (hzrd149). - **Key rotation still unsolved, deliberately**: the field is converging on social recovery / attestation-based migration (Constant's expiring musig2 association events; Malmi's keeper-device + per-device keys; Gigi/Pablo's "your friends tell people it's actually you") rather than any formal scheme. "You are not your npub. Your npub is a pointer." - **Provable time**: OpenTimestamps integration (NIP-03 → Constant's NIP-3B/3C: stamp hash(id+sig), embed the proof) as the defense of history against key compromise and retrospective fabrication. - **Permissioned environments from permissionless blocks**: Constant's TEPP / Kidstr (guardian-managed lists of people/places/things enforced at signer, client, and relay) as the freedom-tech answer to age-verification and digital-ID law — expect this whole area (child safety, SFW modes, supervised onboarding) to grow as regulation lands. ## Networking beyond the social layer - **Nostr keys as network identity**: FIPS (mesh routing to a pubkey instead of an IP; peer discovery and NAT traversal over relays), Malmi's Nostr VPN (WireGuard negotiated over relays, peers addressed by npub, Cashu-incentivized exit nodes), Hashtree (content-addressed filesystem + git on Blossom, convergent encryption for host deniability). The pattern: Nostr as the *coordination and identity plane* for arbitrary protocols — "DNS and IP are the worst offenders" of internet centralization. - **Money in-band**: Cashu/NIP-60 wallets as events (your ecash follows you), nutzaps as self-verifying payment receipts, HTTP-402/bearer-token flows replacing negotiation dances, DVMs as pay-per-use compute. The restaurant model of monetization: sell something actually scarce, no compliance department. ## The AI convergence The loudest new thesis of 2025–26, and it cuts both ways: - **Nostr as agent substrate**: "a neutral substrate that has money, identity, cryptography, and web-of-trust baked in" (Pablo). Agents mint their own npubs, hold NIP-60 wallets, discover tools via NIP-89/90, pay authors of signed knowledge that helped them. Signed provenance becomes existential as AI slop compounds — maintainer-signed docs and snippets outrank the model's own priors. - **AI as adoption lever**: agent-assisted building collapses the cost of the micro-clients the philosophy always wanted (Malmi writes "basically zero" code by hand; Constant ships constantly as a self-described non-programmer; 80% of a SEC workshop shipped a working applet). Hodlbod's hope: AI-assisted hacking of open protocols "tears down walled gardens"; his warning, seconded by everyone: AI-built apps that skip routing, handlers, and WoT are architecturally hollow and quietly re-centralize. - **The training-data window**: "we have this window of opportunity to steer the LLMs and the future of technology into a freedom-tech direction" (Malmi) — publish your thinking where models will learn it, or the next generation of tools knows only Stripe, Cloudflare, and GitHub. Early evidence it works: agent workflows "were always trying to do client-server, REST, username-password… now Nostr is in the training data" (Sandwich). ## Open problems, honestly stated 1. Key rotation / compromise recovery — social schemes sketched, nothing deployed at scale. 2. Outbox-model completeness — most clients still don't fully implement discovery, migration, or re-broadcast; fiatjaf's two-notes experiment still indicts the ecosystem. 3. Blossom link-healing and content longevity (the "Nostr pacemaker" daemons) — obvious, unbuilt. 4. Bootstrapping trust for relay discovery (NIP-66 + WoT) — early. 5. Large-group private messaging — MLS (White Noise/marmot) vs double-ratchet trade-offs unresolved; group privacy at scale may be unachievable in principle. 6. Monetization for creators who need it today — "if you are starving, might as well come and starve here" (Constant). The honest pitch is pioneering, not parity. 7. The DM question — whether encrypted private communication belongs on Nostr at all (Constant: publications protocol; Malmi: no-network-effect privacy is exactly where Nostr *can* win; hzrd149: use Nostr as lookup, let Signal-class protocols carry the payload). === FILE: https://npub1d70emggs6jzun5lhvqnfqsd9reqmaarn2qjf6q3r02gryyl4v8sqjn44xe.nsite.lol/docs/design/design-synthesis.md === > **Nostr Agent Onboarding** · [start here](https://npub1d70emggs6jzun5lhvqnfqsd9reqmaarn2qjf6q3r02gryyl4v8sqjn44xe.nsite.lol/start.md) · [index](https://npub1d70emggs6jzun5lhvqnfqsd9reqmaarn2qjf6q3r02gryyl4v8sqjn44xe.nsite.lol/llms.txt) · source: `nostr-dev/docs/design-synthesis.md` · snapshot 2026-10-10 # Designing solutions on Nostr — synthesis > Codification of the design principles for Nostr applications. Synthesized from > Hodlbod's "Building Nostr" book (Coracle), fiatjaf's writing on fiatjaf.com, > NIP-01, the NIPs kind registry, the OpenSats client report, the Nostr Rising > podcast series, and the Gossip / Outbox literature. > > This is the first document any Claude session in this workspace should read > before designing or building anything Nostr-shaped. It is the "why" layer. > `CLAUDE.md` is the "what is installed" layer. [`docs/kind-cheatsheet.md`](https://npub1d70emggs6jzun5lhvqnfqsd9reqmaarn2qjf6q3r02gryyl4v8sqjn44xe.nsite.lol/docs/design/kind-cheatsheet.md) is the > "what to do when picking a kind" layer. ## 1. The single load-bearing idea Nostr's whole shape falls out of one move: **signatures decouple authentication from storage**. The signed event is the unit of truth. Relays are interchangeable mirrors. The address space is keypairs. Everything else — outbox routing, replaceable events, NIP-05, GRASP — is a downstream consequence. When you design for Nostr, you are not designing a system. You are designing: 1. an event schema (kind + tags + content shape), and 2. a routing strategy (which relays this kind goes to / comes from). The runtime is whatever relays happen to be around. This is also the trap. People keep trying to reintroduce centralization — a canonical relay, a global index, a "main" feed — because eventual-consistency- across-anonymous-mirrors is genuinely uncomfortable. **Resist that.** Lean into "many hackish attempts" (fiatjaf's phrase, see [`sources/fiatjaf-vision.md`](https://npub1d70emggs6jzun5lhvqnfqsd9reqmaarn2qjf6q3r02gryyl4v8sqjn44xe.nsite.lol/docs/sources/fiatjaf-vision.md)) as a feature, not a bug. ### Worked example: same idea, applied to packet routing (FIPS) The cleanest demonstration that this load-bearing idea generalizes outside Nostr's own event/relay surface is **FIPS** (Free Internetworking Peering System; [`sources/fips.md`](https://npub1d70emggs6jzun5lhvqnfqsd9reqmaarn2qjf6q3r02gryyl4v8sqjn44xe.nsite.lol/docs/sources/fips.md)). FIPS is a decentralized mesh routing protocol whose nodes are identified by **the same secp256k1 keypairs as Nostr nodes**. Peer discovery, endpoint advertisement, and NAT-traversal signaling all happen over Nostr relays (kind 37195 addressable adverts; kind 21059 NIP-59-wrapped offers/answers). Where Nostr decouples authentication from event storage, FIPS decouples it from packet routing — but the underlying move is identical: signed thing + interchangeable mirrors. If you're designing anything that wants "Nostr keypair as identity" but isn't itself a Nostr application, FIPS is the most fully-developed reference architecture available, and its 13 layered design specs in `repos/fips/docs/design/` are worth reading on their own as a protocol-design exercise. ## 2. Routing: relay-feed vs outbox vs gossip Three positions on the same axis, increasing sophistication. Pick deliberately; don't drift. ### Relay-feed (fixed list) The simplest model. The client connects to a curated set (typically 3–10 relays), publishes there, reads there. Easy to reason about; predictable. Costs: - **Centralization** — popular relays carry everything; the long tail dies. - **Brittle to censorship** — if the relays in your fixed set drop you, you're gone, and your followers can't find you. - **Doesn't scale** — you either over-fetch (connect to thousands of relays defensively) or under-fetch (miss the author's actual data). Right choice when: the relay set is *intentional* and *bounded*. A community relay, a private/team relay, a topical feed, a paid index. Not the right choice for a general-purpose social client. ### Outbox (NIP-65, kind 10002) Each user advertises their relays via a kind 10002 event. The two flags: - `write` ≡ **OUTBOX** — where they publish. - `read` ≡ **INBOX** — where DMs/mentions should be sent. Routing rules that fall out of this: | Action | Where to write | Where to read | |---|---|---| | Read someone's posts | — | their **OUTBOX** | | Send a DM / mention | recipient's **INBOX** + your own INBOX (for record) | — | | Publish your own post | your **OUTBOX** + tagged users' **INBOX** | — | | Publish a profile / follow list / relay list | your **OUTBOX** | — | The honest framing from the literature: outbox is **necessary but not sufficient**. NIP-65 only solves "who-publishes-where for kind:1-style social." Communities, DMs, topical feeds, search/indexing relays — none of those are solved by NIP-65. ### Gossip (Mike Dilger's superset) Outbox plus every other discoverable relay hint: - `nprofile` URIs that include relay hints, - NIP-05 metadata that names relays, - relay hints in `e`/`p`/`a` tags on events that mention people, - observed presence of authors in replies on relays you already speak to. The Gossip client (mikedilger/gossip) is the canonical reference. NDK and Coracle both implement gossip-flavored outbox. If you build a serious client, you want gossip-flavored outbox. If you build a tool or pipeline (a bot, a specialty relay, a single-purpose client), a fixed list is often fine — be explicit about why. ### Per-kind routing — the real design surface Once you accept "no single relay set fits all jobs," the design question becomes **per-event routing**: - Public broadcast (kind 1 notes, reposts, reactions) → outbox + tagged inboxes. - DMs (NIP-17 / NIP-44 / NIP-59) → inbox-only of recipient. - Community / group content (NIP-29, NIP-72) → community relay only. - Topical / curated feeds → topic relay (paid, vetted, or invite-only). - Search / indexing (kind 30000-range listings) → indexer relay (`purplepag.es`, `user.kindpag.es`, `search.nos.today`; `relay.nostr.band` was unreachable as of 2026-09). - GRASP / nostr-git events (kind 30617/30618/1617/1621/1622) → repo's GRASP relay first, fall back to user's outbox. A client that does all five well is hard. A client that does one of them brilliantly is the Nostr way. ## 3. Event kinds — taxonomy and decision rule NIP-01 defines four classes by kind number. The relay's storage contract is *different for each class*. This is the key design choice; it is also where most people make the wrong call. | Class | Range | Relay contract | Use for | |---|---|---|---| | **Regular** | 1–2, 4–44, 1000–9999, 40000+ | Store everything; full history. | Notes, reactions, deletions, reposts. **Default.** | | **Replaceable** | 0, 3, 10000–19999 | Latest per `(pubkey, kind)`. | Single-current-value-per-user state: profile (0), follow list (3), relay list (10002). | | **Ephemeral** | 20000–29999 | Not stored. Live broadcast only. | Live signaling: typing, in-flight signing (NIP-46), presence. | | **Addressable** | 30000–39999 | Latest per `(pubkey, kind, d-tag)`. | Multi-keyed state: long-form by slug (30023), git repos by name (30617), lists. | ### Default to regular This is the consistent advice across Hodlbod, fiatjaf, and the Coracle book. > Regular events have history, are auditable, and degrade gracefully. The > other three classes are optimizations that throw information away — only > reach for them when you can articulate *why* throwing it away is the right > call. ### When to reach for each non-regular class - **Replaceable** — there is exactly one current value per user, and history is noise, not signal. Your relay list, not "all the relay lists you've ever had." Your profile picture, not a timeline of profile pictures. - **Addressable** — same as replaceable, but you have multiple distinct items keyed by an identifier *you* control: one article per slug, one repo per name, one list per d-tag. Replaceable + cardinality. - **Ephemeral** — persistence would actively be wrong. A "user X is typing" event that lives forever is bizarre. A NIP-46 in-flight signing request that hangs around for months is dangerous. Ephemerals are the right call when the event has only an instantaneous meaning. - **Regular** — everything else. Including: things you might be tempted to make replaceable for "efficiency." If you want history, accountability, threading, or "who said what when" — use regular. ### Common smells - A "settings" event that's regular and accumulates → should be replaceable. - A "current location" event that's regular → should probably be ephemeral. - A "blog post" that's regular and edited → should be addressable. - A "comment" that's addressable so it can be edited → almost certainly wrong; comments are accountable, use regular and tombstone with a NIP-09 deletion if needed. - A "draft" that's regular → should be addressable (one current draft per user-defined identifier). ## 4. Picking a kind — concrete recipe The instinct "check before you allocate" is the right one. The procedure: 1. **Read the canonical kind table** in [`nostr-protocol/nips/README.md`](https://github.com/nostr-protocol/nips/blob/master/README.md). This is the registry. (See vendored copy in [`docs/sources/nips-readme-kinds.md`](https://npub1d70emggs6jzun5lhvqnfqsd9reqmaarn2qjf6q3r02gryyl4v8sqjn44xe.nsite.lol/docs/sources/nips-readme-kinds.md) for offline reference.) 2. **Use NAK locally** — it ships with NIP descriptions: ``` nak nip # list all NIPs nak knows nak nip 34 # show NIP-34 (git stuff) nak nip open 34 # open NIP-34 in the browser ``` On this machine NAK knows about 91 NIPs. 3. **Probe the live network** to see whether a kind is in use *even if undocumented*: ``` nak req -k 39999 -l 1 wss://relay.damus.io wss://nos.lol ``` This is the most underrated step. Plenty of clients ship private kinds that aren't in the registry but show up everywhere. We confirmed kind 39999 is occupied in the wild even though no NIP claims it. 4. **Reuse before you invent.** If your data fits an existing schema, use the existing kind. Inventing a new kind for "long-form content" because you don't like NIP-23 fragments the ecosystem and no one will read your events. This is the single biggest mistake new builders make. 5. **For new public kinds** — open a PR against [`nostr-protocol/nips`](https://github.com/nostr-protocol/nips). The community process is informal but real. For a private/in-house kind that only your client cares about, pick a high number in an unallocated band, document it in your repo, and accept that other clients will ignore it. A practical 30-second cheatsheet lives in [`docs/kind-cheatsheet.md`](https://npub1d70emggs6jzun5lhvqnfqsd9reqmaarn2qjf6q3r02gryyl4v8sqjn44xe.nsite.lol/docs/design/kind-cheatsheet.md). ## 5. Architectural philosophy — distilled From Nostr Design's "Guiding Principles," fiatjaf's posts, Hodlbod's book, and the OpenSats report: - **User sovereignty above all.** Control of keys, choice of relays, no lock-in. - **Signatures decouple data from storage.** Same event valid anywhere; relays are interchangeable mirrors, not authorities. - **Build small things that do one thing well.** Modular clients > monolithic platforms. ("A client that does one thing really well.") - **Embrace the messiness.** There is no global state. Discovery is "many hackish attempts." Eventual consistency is the norm. - **Don't censor; expose filtering.** Give users tools, don't decide for them. - **Per-relay context matters.** User-specific (outbox), public-feed (broadcast), topical/community (curated), DMs (inbox-only). One relay set doesn't fit all jobs. ## 6. What this means for building solutions on Nostr The design surface is four decisions, taken in this order: ### 1. What's the event schema? A kind, a tag set, a content shape. This is the only schema you get; **tags are your data model.** Reuse existing kinds and tag conventions wherever you can. Document any new ones in your repo. ### 2. Which class of event? Default regular. Justify any other choice in writing — what are you optimizing, what are you discarding? ### 3. What's the routing strategy per kind? - Outbox for broadcast, - inbox for delivery, - topic relay for community, - index relay for search. Don't conflate. ### 4. What's the failure model? Events arrive duplicated. Events don't arrive at all. Events arrive late, out of order, or from a relay that's lying. **Build for that:** - idempotent ingest, - dedup by event id, - treat relay responses as "best effort," - never assume "I queried 3 relays and got nothing, therefore it doesn't exist." The recurring theme: **don't build platforms, build pieces.** A client that does one thing well, against an event schema that other clients can also speak. Censorship-resistance, identity portability, and the entire point of the protocol fall out of that. Try to build a "Nostr Twitter" and you'll fight the protocol; build a Nostr-native long-form reader, a code-collaboration tool, or a feed algorithm, and the protocol carries you. ## 7. Local sources Vendored where licensing allows; summaries + links otherwise. See `docs/sources/`: - `nip-01.md` — NIP-01 verbatim (MIT, the canonical event-kind classification) - `nips-readme-kinds.md` — the NIPs README kind registry table - `fiatjaf-vision.md` — summary + link to fiatjaf's "A vision for content discovery" - `outbox-model.md` — summary + links to Nostrify, Why Nostr, gossip docs - `hodlbod-book.md` — pointer to building-nostr (cloned into `repos/` for offline read) - `nostr-rising.md` — episode list + summaries of the Nostr Rising series - `nostr-design-principles.md` — Nostr Design "Guiding Principles" summary - `opensats-advancements.md` — OpenSats "Advancements in Nostr Clients" summary - `videos.md` — pointers to fiatjaf / Hodlbod / Pablo / Dilger talks on YouTube === FILE: https://npub1d70emggs6jzun5lhvqnfqsd9reqmaarn2qjf6q3r02gryyl4v8sqjn44xe.nsite.lol/docs/design/kind-cheatsheet.md === > **Nostr Agent Onboarding** · [start here](https://npub1d70emggs6jzun5lhvqnfqsd9reqmaarn2qjf6q3r02gryyl4v8sqjn44xe.nsite.lol/start.md) · [index](https://npub1d70emggs6jzun5lhvqnfqsd9reqmaarn2qjf6q3r02gryyl4v8sqjn44xe.nsite.lol/llms.txt) · source: `nostr-dev/docs/kind-cheatsheet.md` · snapshot 2026-10-10 > > Paths such as `~/Documents/…`, `~/Production Environment/…`, `repos/…` and services on `localhost` refer to the author's workstation and are **not available to you** — read them as worked examples of a setup you can recreate. # Picking a Nostr event kind — 30-second cheatsheet > Companion to [`docs/design-synthesis.md`](https://npub1d70emggs6jzun5lhvqnfqsd9reqmaarn2qjf6q3r02gryyl4v8sqjn44xe.nsite.lol/docs/design/design-synthesis.md) §3–4. When you need to ship. ## Step 0 — Default Use **regular** (kind in `1–2`, `4–44`, `1000–9999`, or `40000+`) unless you have a written reason not to. ## Step 1 — Class ``` one-current-value-per-user? / \ yes no | | keyed by a d-tag? ephemeral signal? / \ / \ yes no yes no | | | | ADDRESSABLE REPLACEABLE EPHEMERAL REGULAR ← default 30000-39999 10000-19999 20000-29999 1-9999, 40000+ ``` ## Step 2 — Existing kind? Use it. Before allocating, do all four checks: ```bash # 1. Search the canonical registry. $EDITOR ~/Documents/nostr-dev/docs/sources/nips-readme-kinds.md # or upstream: https://github.com/nostr-protocol/nips/blob/master/README.md # 2. Ask NAK what NIPs it knows. nak nip # list nak nip 34 # show one nak nip open 34 # open in browser # 3. Probe the live network for events of that kind. # Even undocumented kinds may be in use by clients. # Real-world example: kind 37195 is FIPS overlay-discovery adverts (the # digits spell "FIPS" — 7=F, 1=I, 9=P, 5=S). Not in the canonical # NIPs registry, but live on relay.damus.io and nos.lol. Anyone who # allocated 37195 without probing would have collided with FIPS. nak req -k -l 3 wss://relay.damus.io wss://nos.lol wss://relay.primal.net # 4. Probe with a tag filter to see realistic schemas. nak req -k -l 5 wss://relay.primal.net | jq -c '{kind,tags}' ``` If anyone is using it, your options are: (a) match their schema, (b) pick a different number, (c) propose a NIP that consolidates. ## Step 3 — If you must invent - Pick a number in a *gap* of the appropriate class. The README registry has many. - Document it in your repo with: kind, class (regular/replaceable/ephemeral/ addressable), tag schema, content shape, *why* you chose this class. - For anything you want other clients to interoperate with, open a PR against [`nostr-protocol/nips`](https://github.com/nostr-protocol/nips). The community process is informal but real. ## Step 4 — Tag conventions matter as much as kinds The kind says "what shape." The tags say "what data." Reuse before you invent: | Tag | Meaning | |---|---| | `e` | reference to another event id | | `p` | reference to a pubkey | | `a` | reference to an addressable event (`::`) | | `d` | identifier for an addressable event | | `t` | hashtag / topic | | `r` | URL or relay reference | | `k` | kind (used in NIP-22 comments to point at parent kind) | | `client` | client name + version | | `alt` | accessibility / fallback rendering | | `expiration` | unix timestamp after which the event should be considered expired | | `subject` | thread title (NIP-14) | If your data is "a reference to a thing + an annotation," it's almost always **existing tag + new content**, not new kind. ## Common smells (recap from the synthesis) - A "settings" event that's *regular* and accumulates → should be **replaceable**. - A "current location" event that's *regular* → should probably be **ephemeral**. - A "blog post" that's *regular* and edited → should be **addressable**. - A "comment" that's *addressable* so it can be edited → almost certainly wrong; comments are accountable, use **regular** and tombstone with NIP-09. - A "draft" that's *regular* → should be **addressable** (one current draft per user-defined identifier). ## Quick reference — well-known kinds you'll meet | Kind | What | Class | NIP | |---|---|---|---| | 0 | User metadata (profile) | replaceable | 1 | | 1 | Short text note | regular | 1 | | 3 | Follow list (contact list) | replaceable | 2 | | 4 | Encrypted DM (legacy, deprecated for NIP-17) | regular | 4 | | 5 | Event deletion request | regular | 9 | | 6 | Repost | regular | 18 | | 7 | Reaction | regular | 25 | | 9 | Chat message | regular | 28 | | 16 | Generic repost (non-kind-1) | regular | 18 | | 1059 | Gift wrap (privacy envelope) | regular | 59 | | 1063 | File metadata | regular | 94 | | 5128 | **nsite manifest snapshot** | regular | 5A | | 1311 | Live chat message | regular | 53 | | 1617 | Patch (git) | regular | 34 | | 1621 | Issue (git) | regular | 34 | | 1622 | Reply (git issue / PR) | regular | 34 | | 1630–1633 | Status (git, open/closed/merged/draft) | regular | 34 | | 9734/9735 | Zap request / receipt | regular | 57 | | 10000 | Mute list | replaceable | 51 | | 10002 | **Relay list (outbox/inbox)** | replaceable | 65 | | 10006 | Pinned events | replaceable | 51 | | 10019 | **Nutzap mint list** | replaceable | 61 | | 10063 | **Blossom server list** (user's media servers) | replaceable | BUD-03 | | 15128 | **Root nsite manifest** (static site; MUST NOT carry `d`) | replaceable | 5A | | 17375 | Cashu wallet | replaceable | 60 | | 22242 | Auth (NIP-42 client auth challenge) | ephemeral | 42 | | 24133 | NIP-46 connect (Nostr Connect / bunker) | ephemeral | 46 | | 27235 | HTTP auth (NIP-98) | ephemeral | 98 | | 30000 | Follow set | addressable | 51 | | 30002 | Relay set | addressable | 51 | | 30008 | Profile badge | addressable | 58 | | 30009 | Badge definition | addressable | 58 | | 30023 | Long-form article | addressable | 23 | | 30024 | Long-form draft | addressable | 23 | | 30311 | Live event | addressable | 53 | | 30402 | Classified listing | addressable | 99 | | 30617 | **Git repository announcement** | addressable | 34 | | 30618 | Git repository state | addressable | 34 | | 31922 | Date-based calendar event | addressable | 52 | | 34128 | ~~Legacy nsite per-path event~~ — **deprecated, relays refuse it** | addressable | 5A | | 35128 | **Named nsite manifest** (`d` = `^[a-z0-9-]{1,13}$`) | addressable | 5A | | 31923 | Time-based calendar event | addressable | 52 | | 32267 | **Software application** (Zapstore app metadata, `d` = package id) | addressable | 82 (PR) | | 30063 | **Software release set** (Zapstore, `d` = `@`) | addressable | 82 (PR) | | 3063 | **Software asset** (Zapstore per-binary file metadata; *not* NIP-94's 1063) | regular | 82 (PR) | | 30509 | **Cryptographic identity link** (NIP-C1, `d` = cert SHA-256) — binds an APK signing key to a Nostr identity | addressable | C1 | | 39000–39009 | Group metadata (NIP-29) | addressable | 29 | For anything not on this list: do Step 2. === FILE: https://npub1d70emggs6jzun5lhvqnfqsd9reqmaarn2qjf6q3r02gryyl4v8sqjn44xe.nsite.lol/docs/design/known-issues.md === > **Nostr Agent Onboarding** · [start here](https://npub1d70emggs6jzun5lhvqnfqsd9reqmaarn2qjf6q3r02gryyl4v8sqjn44xe.nsite.lol/start.md) · [index](https://npub1d70emggs6jzun5lhvqnfqsd9reqmaarn2qjf6q3r02gryyl4v8sqjn44xe.nsite.lol/llms.txt) · source: `nostr-dev/docs/known-issues.md` · snapshot 2026-10-10 > > Paths such as `~/Documents/…`, `~/Production Environment/…`, `repos/…` and services on `localhost` refer to the author's workstation and are **not available to you** — read them as worked examples of a setup you can recreate. # Known issues — local stack quirks > Issues a sibling agent encountered while building `~/work/nostr-archive/` > against this workspace's local stack. Documented here so future sessions > don't re-discover them. Each entry: what breaks, why, the workaround on this machine, and (if applicable) the upstream fix to track. ## 1. ngit-relay nginx emits CORS headers twice **Symptom.** A browser fetching anything from `http://localhost:8081` fails with: ``` Access-Control-Allow-Origin: * (multiple) The 'Access-Control-Allow-Origin' header contains multiple values... ``` The browser blocks the response. `curl -i` shows two `Access-Control-Allow-Origin: *` headers in the response. **Cause.** The container's `src/nginx.conf` adds CORS headers via `add_header` inside `location @proxy { ... }`, *and* the upstream Khatru server (proxied to `localhost:3334`) also emits the same headers. Nginx passes both through — the response ends up with duplicate CORS headers, which all major browsers reject per the CORS spec. **Affects.** Any browser-based client hitting the local GRASP / Blossom server. The TS examples in [`sdk/examples/`](https://npub1d70emggs6jzun5lhvqnfqsd9reqmaarn2qjf6q3r02gryyl4v8sqjn44xe.nsite.lol/examples/README.md) don't trigger it because they run in Node (no browser CORS check). It surfaces the moment you serve a static page from one origin and hit `localhost:8081` from the page's JS. **Workaround (runtime, applied per container start).** Add five `proxy_hide_header` lines inside `location @proxy` so nginx strips Khatru's headers and keeps only its own: ```bash docker exec ngit-relay-ngit-relay-1 sh -c ' sed -i "/location @proxy {/a\\ proxy_hide_header Access-Control-Allow-Origin;\\ proxy_hide_header Access-Control-Allow-Methods;\\ proxy_hide_header Access-Control-Allow-Headers;\\ proxy_hide_header Access-Control-Expose-Headers;\\ proxy_hide_header Access-Control-Max-Age; " /etc/nginx/http.d/default.conf && nginx -s reload ' ``` Verify with: ```bash curl -sI -H 'Origin: http://localhost:9000' http://localhost:8081/list/0000000000000000000000000000000000000000000000000000000000000000 \ | grep -i access-control-allow-origin # expected: exactly one line ``` **Note.** This patch is **runtime only**. If you `sudo docker compose down` and `up`, the patch is gone — re-apply. To make it persistent, edit `repos/ngit-relay/src/nginx.conf` and `docker compose up -d --build`. We deliberately don't patch the source in-place because `repos/ngit-relay` is a clone of the upstream reference implementation and we want it to track upstream cleanly. **Discovered by.** The sibling agent building `~/work/nostr-archive/`'s browser client. See `~/work/nostr-archive/client/README.md` § "Known nginx CORS quirk in ngit-relay" for the original report. **Upstream.** Worth filing on the ngit-relay project. The fix is one config block; the regression is browser-only so the test matrix didn't catch it. ## 2. Khatru evicts kind:30617 events by `(pubkey, kind)` ignoring `d`-tag **Symptom.** Re-publishing a kind:30617 GRASP repo announcement under one `d`-tag silently evicts a different repo's kind:30617 by the same pubkey. Querying for the *previous* repo returns nothing, even though the announcement was previously visible. **Cause.** Per NIP-01, kinds 30000–39999 are *addressable*: relays must keep the latest event per `(pubkey, kind, d-tag)`. The Khatru implementation packaged in the local ngit-relay appears to evict on `(pubkey, kind)` only — treating addressable events as if they were plain replaceable. **Affects.** Any pubkey publishing more than one GRASP repo to the local relay. Single-repo workflows don't see this. **Reproducer (informal).** ``` nak event --sec $NSEC -k 30617 -t d=alpha ws://localhost:8081 nak event --sec $NSEC -k 30617 -t d=beta ws://localhost:8081 # both queryable nak event --sec $NSEC -k 30617 -t d=alpha ws://localhost:8081 # 'beta' is now gone in the buggy version ``` **Workaround.** **Also publish each kind:30617 to a second relay that handles addressable events correctly.** A standalone `nostr-rs-relay` on `:8080` (already part of this workspace, see `configs/local-services.json` → `available_on_demand.nostr_rs_relay`) is a clean choice. Or use a public relay (`wss://relay.ngit.dev`, `wss://relay.damus.io`). The public relay you replicate to acts as the source of truth for multi-repo announcements; the local relay still serves git pushes for whatever repos *it* still has. If the bug is in your specific Khatru build, mirroring is also the diagnostic — when the local query disagrees with the public query, you've hit it. **Upstream.** Track via the Khatru codebase (`github.com/fiatjaf/khatru`) and the eventstore module the local container uses (`fiatjaf/eventstore` — see `relays/khatru-example/go.mod` for the version pinned in our example relay). The eviction logic for addressable events lives in the eventstore backend (`slicestore`, `lmdb`, etc.) — different backends may have different behavior. **Discovered by.** The sibling agent building `~/work/nostr-archive/`, which publishes one kind:30617 per archive and one per archive's GRASP-as-structural-layer (so two per archiver, same pubkey, same kind, different d-tag). ## 3. `relay.zapstore.dev` whitelist verifier does not follow NIP-34 / GRASP repos **Symptom.** Publishing kind 32267 / 30063 / 3063 events to `wss://relay.zapstore.dev` is rejected with: ``` event pubkey is not allowed. Visit https://zapstore.dev/docs/publish for more information. ``` Committing a `zapstore.yaml` containing `pubkey: ` at the root of the linked source repo — the official path documented at — does **not** lift the rejection when the source repo is a NIP-34 GRASP repo. The relay continues to refuse. Kind 30509 (NIP-C1 cert link) and kind 0 (profile) under the same pubkey are accepted; only the app-event trio is gated. **Cause.** The relay's auto-whitelist verifier only resolves repository references against a fixed set of forge hosts — confirmed against [the publish docs](https://zapstore.dev/docs/publish): > auto-whitelisting works for repositories hosted on GitHub, GitLab, > Codeberg, and self-hosted Gitea/Forgejo instances. Even though kind 32267's `repository` tag is a valid NIP-34 `naddr1…`, the verifier does not (yet) resolve it to a kind 30617, follow the announced `clone` URLs, and fetch `zapstore.yaml` from the resulting git tree. GRASP-hosted projects therefore can never auto-whitelist via the yaml-in-repo flow. **Affects.** Any publisher whose canonical source repo is a GRASP repo — i.e., the canonical configuration of *this* workspace. Three options remain: 1. **Mirror to a supported forge.** Push the source to `github.com//` (or Codeberg, GitLab, Gitea, Forgejo) with the same `zapstore.yaml` at root. Re-issue the app events; the verifier reads the yaml from the forge, matches pubkey↔repo, and admits future publishes from that pubkey. Requires keeping the forge mirror in sync on every release. 2. **Vertex reputation.** Accumulate Nostr social activity under the publisher pubkey until their reputation system admits it. Passive; not directly actionable. 3. **Skip `relay.zapstore.dev`.** Three general-purpose relays (`relay.damus.io`, `nos.lol`, `relay.primal.net`) accept the same events with no whitelist gate. Any outbox-model-aware Zapstore client not pinned exclusively to the Zapstore relay will find the app from those. **Discovered by.** This workspace, 2026-05-12, publishing OTSuite Mobile v2.0.0 under `npub1zkd4h6zshlh2a3yuzaxqk2wgk2kv2za5trz76rd5upwy06fscjrq8mk5ta`. The repo had its kind 30617 / 30618 on `relay.ngit.dev`, `zapstore.yaml` with `pubkey: …` was committed (`6444c9d…`), and the public clone URL was reachable — the relay still rejected. Cert-link (30509) and profile (0) under the same pubkey were accepted. **Upstream.** Worth filing on the `zapstore` org — implementing naddr-aware verification would close the gap. Track at (there is no dedicated relay repo public at the time of writing; the `zsp` CLI is the closest published artifact). ## 4. `relay.ngit.dev` rejects events that don't carry an NIP-34 `a`-tag **Symptom.** Pushing zapstore-style events (kind 30509, 30063, 3063, or even kind 0) to `wss://relay.ngit.dev` under a pubkey that *is* the maintainer of a stored GRASP repo returns: ``` Event event must reference an accepted repository or accepted event ``` Only kind 32267 lands, because `zsp` emits it with an `a`-tag containing the kind 30617 coordinate of the linked repo. **Cause.** `relay.ngit.dev` runs the same Khatru policy as the local `ngit-relay` container (`ws://localhost:8081`): an event is only stored if it explicitly references a stored repo by NIP-34 coordinate (`a`-tag `30617::`) or by event id of an already-accepted event (`e`-tag). Zapstore's release/asset/identity events reference the app by `i`-tag (package id) instead, which the policy doesn't recognize. By design — it's a *git repository* relay, not a general-purpose one — but the selective acceptance is surprising on first contact. **Affects.** Any flow that publishes non-NIP-34 events under a pubkey whose primary identity is "GRASP repo maintainer." The most concrete case is the zapstore publish documented in [`patterns/zapstore-publishing.md`](https://npub1d70emggs6jzun5lhvqnfqsd9reqmaarn2qjf6q3r02gryyl4v8sqjn44xe.nsite.lol/docs/patterns/zapstore-publishing.md): four of five event kinds bounce off `relay.ngit.dev` even though they're all signed by the same npub that maintains the linked repo. **Workaround.** Skip `relay.ngit.dev` for non-repo-tagged events; the three general-purpose relays carry them fine. Hand-patching each event to include an `a`-tag is possible but rewrites the event id (different content → different sha256 → different signature), which then diverges from the canonical event on every other relay — not worth it. **Discovered by.** Same session as issue #3. **Upstream.** Khatru policy in the `ngit-relay` reference implementation (`repos/ngit-relay/`). Relaxing the rule to accept e.g. "any event from a pubkey that maintains a stored repo, regardless of tag form" would close this gap. Track via the `ngit-relay` project. ## 5. `nak event --sec ...` silently re-signed fully-formed events on stdin — RESOLVED in nak v0.19.2 **Resolved upstream.** nak commit [`faac4d9`](https://github.com/fiatjaf/nak/commit/faac4d9440368fcad7f46c077089cc10fe37d81f) (2026-03-17, "ensure an event is not resigned if it was already signed and wasn't changed") first shipped in **v0.19.2**, tagged 2026-03-20. The `nak` on `PATH` is v0.20.7 (§7.1), so it has the fix. The behaviour in §5.1 started in v0.7.9 (commit `134d122`, 2024-10-29) and lasted **through v0.19.1**. **The rule now.** A piped event that already carries a `sig` and that no flag modifies is printed and published exactly as given. `--sec`, `NOSTR_SECRET_KEY` and `--auth` do not count as modifications, and the key is used only to answer NIP-42 AUTH. nak still re-signs when: - a flag modifies the event: `-k`, `-c`, any tag flag (`-t`, `-e`, `-p`, `-d`, `-h`, `-a `), `--ts`/`--created-at`, `--pow`, `--musig`; - the input is incomplete (no `kind`, `created_at` or `sig`), so nak fills it in; - **`--force-sign`** is given (new in v0.19.2). A re-sign uses the `--sec` key, or nak's per-machine default key if there is none. If that isn't the event's own key, `pubkey`, `id` and `sig` all change: the old trap. The odd one out is `-a `: it overwrites `pubkey` but does **not** re-sign, so the output is an invalid event. **Gift wraps.** An unmodified NIP-59 wrap now goes through `nak event --auth --sec ` intact. Its `pubkey` stays the ephemeral key the seal was encrypted to, so the recipient can decrypt it. `--force-sign`, or any flag that modifies the event, still overwrites that ephemeral pubkey with the `--sec` key and makes the wrap undecryptable (mechanism in §5.1). Never pass either with a wrap. **The reverse trap.** Anything that relied on `--sec` to re-sign now publishes the input unchanged. That includes re-authoring an event under another key and repairing a hand-edited signed JSON. nak checks only that a `sig` is *present*, not that it verifies, so an edited event goes out with its stale `id` and `sig` and relays reject it. Add `--force-sign`. **Pre-flight.** What `nak event` prints is what it would publish. So before publishing a pre-formed event, run the same command without relay URLs and compare. Use `jq -cS .` on both sides, because nak re-serialises JSON that another tool wrote: ```bash diff <(jq -cS . wrap.json) <(nak event --auth --sec "$NSEC" < wrap.json | jq -cS .) \ && echo "publishes as given" ``` For an older `nak`, the TypeScript helper in §5.1 remains the route. **Verified 2026-09-26, offline.** Every command ran inside `bwrap --unshare-net` (loopback only), with no relay argument, so nothing was published. Throwaway keys: A signed the input, B is the other `--sec`, R is the gift-wrap recipient. "Re-signed as B" means pubkey B with a new `id` and `sig`, still valid. v0.18.3 is the parked `~/.local/bin/nak.v0.18.3.bak`. | Piped input → `nak event …` | v0.18.3 | v0.20.7 | |---|---|---| | note signed by A → `--sec B` | re-signed as B | **byte-identical** | | same → `NOSTR_SECRET_KEY=B`, no flag | re-signed as B | byte-identical | | same → `--sec A` (its own key) | byte-identical | byte-identical | | same → `--sec B --auth` | not run | byte-identical | | same → `--sec B -t x=y` | not run | re-signed as B | | same → `--sec B --force-sign` | (no such flag) | re-signed as B | | same → `--sec B -a ` | not run | pubkey B, old `id`/`sig`: **invalid** | | note signed by A, content then hand-edited → `--sec A` | re-signed, valid | passed through, **invalid** | | gift wrap A→R → `--sec B` | pubkey B, **R cannot decrypt** | byte-identical (also with `--auth`), R decrypts | | gift wrap A→R → `--sec B --force-sign` | (no such flag) | pubkey B, **R cannot decrypt** | R's decryption was checked two ways: layer by layer with `nak decrypt`, and with `nak gift unwrap`. A forced re-sign with the event's own key also comes out byte-identical, because signing is deterministic. **Not tested: a live AUTH publish.** From the source: with an event on stdin, `publishFlow` takes its plain (non-TTY) path. On `auth-required:` it calls `relay.Auth(ctx, kr.SignEvent)`, which signs only a kind 22242 AUTH event with the `--sec` key, and then republishes the same, unchanged `evt`. To re-check after a nak upgrade (no relay argument, so nothing is published): ```bash A=$(nak key generate); B=$(nak key generate) nak event -k 1 -c hi --sec "$A" /tmp/a.json nak event --sec "$B" < /tmp/a.json | cmp - /tmp/a.json && echo unchanged # fixed nak event --sec "$B" --force-sign < /tmp/a.json | jq -r .pubkey # → B's pubkey ``` ### 5.1 History: the entry as logged 2026-05-12 (nak v0.7.9 – v0.19.1) Kept as written, apart from the dated notes. **Symptom.** You pipe a complete, signed event into `nak event` and pass `--sec` so the relay's NIP-42 AUTH challenge can be answered. nak prints "publishing... success" and the relay accepts the event — but the event that lands on the relay has its `pubkey` rewritten to whatever pubkey corresponds to `--sec`, with a new `id` and a new `sig`. The original `pubkey` is lost. For a NIP-59 gift wrap this is silently *catastrophic*: the encrypted content (the kind 13 seal) was created with `ECDH(ephemeral_priv, recipient_pub)`. The recipient decrypts by computing `ECDH(recipient_priv, event.pubkey)`. When nak overwrites `event.pubkey` with the maintainer's identity, `event.pubkey` no longer matches the key the ciphertext was sealed for — NIP-44 v2 decryption fails and the message appears as undecryptable garbage on the recipient's side. **Cause.** `nak event` documents that a piped event "is rehashed and resigned if modified, otherwise just returned as given." The modification detector treats a `--sec` whose derived pubkey differs from `event.pubkey` as a request to change `event.pubkey`, then dutifully rehashes and re-signs. There is no warning, no `--no-rehash` flag, and the printed output is the re-signed event (visible only with `--verbose` during a publish). *Correction, 2026-09-26: the source shows a wider trigger.* From `134d122` through v0.19.1, `event.go` set `mustRehashAndResign` whenever `--sec` or `--prompt-sec` was set at all, and `NOSTR_SECRET_KEY` in the environment counted too. Nothing compared the key with `event.pubkey`. With the event's own key the re-sign was invisible, because deterministic signing gives byte-identical output. With any other key, `pubkey`, `id` and `sig` were rewritten. The effect was as described above. **Affects.** Any pipeline of the form ```bash nak event --sec < some-event.json ``` where `some-event.json` was produced by another tool and its `pubkey` is not the same as the one derived from ``. NIP-59 gift wraps are the canonical victim (ephemeral wrap key by design). Same trap applies to NIP-46 bunker responses you proxy, decoupled-key flows, anything signed by an ephemeral session key, or events you're republishing from a relay on someone else's behalf. *Note, 2026-09-26:* the same pipe with no `--sec` flag was hit too whenever `NOSTR_SECRET_KEY` was exported (measured on v0.18.3). **Reproducer.** ```bash # Pre-formed event with one pubkey... nak event -k 1 -c hi --sec /tmp/a.json jq -r .pubkey /tmp/a.json # → pubkey of A # ...piped through nak with a *different* --sec for AUTH only nak --verbose event --sec wss://relay-needing-auth < /tmp/a.json \ | grep -m1 pubkey # → pubkey of B (silently rewritten), id changed, content preserved ``` **Workaround.** Don't pass `--sec` to `nak event` when publishing a pre-formed event. For relays that demand NIP-42 AUTH this means you have to handle AUTH yourself — and this workspace ships exactly that helper: ```bash # In sdk/examples/publish-wrap-with-auth.ts: NOSTR_SECRET_KEY= npx tsx examples/publish-wrap-with-auth.ts \ /tmp/event.json wss://auth-required-relay [more-relays] ``` The helper opens a raw `ws://` connection per relay, waits ~500 ms for a proactive `["AUTH", ]` from the relay (and also handles the `["OK", , false, "auth-required: …"]` follow-up form), signs a kind 22242 AUTH event with the supplied key, retries the EVENT once, and publishes the event **byte-for-byte unchanged**. Reusable for any pre-formed-event + AUTH-relay combo, not just gift wraps. If you need to clean up after hitting this in production, a NIP-09 kind:5 deletion request signed by the *re-signed* event's `pubkey` (i.e. the maintainer key, since that's what nak replaced into the event) referencing the broken event's `id` is what compliant relays will honour. **Discovered by.** This workspace, 2026-05-12, sending a NIP-17 / NIP-59 gift-wrapped DM from the OTSuite Mobile maintainer to the Zapstore admin to request a manual whitelist. The first two attempts went through `nak event --auth --sec ...` and produced an undecryptable wrap; the helper above is the fix. *Note, 2026-09-26:* by then the fix had been out for 53 days (v0.19.2, 2026-03-20). Per §7.1, though, the `nak` on `PATH` until 2026-09-21 was v0.18.3. Before logging a nak bug, compare `go version -m $(command -v nak)` against the latest release. **Upstream.** Worth filing on `fiatjaf/nak`. Either an explicit `--no-rehash` flag, or a default that refuses to silently mutate `event.pubkey` when the input event was already validly signed, would prevent recurrence. *Note, 2026-09-26: already done.* v0.19.2 shipped the second option: signed, unmodified input is no longer re-signed. It checks that a `sig` is present, not that it is valid. `--force-sign` is the explicit opt-in. There is nothing left to file for this issue. The `-a ` case in the rule above is unfiled. ## 6. Signer login (NIP-07 / NIP-46 / NIP-55): relay rot and known-bad code on this machine **Read first:** [`patterns/signer-login.md`](https://npub1d70emggs6jzun5lhvqnfqsd9reqmaarn2qjf6q3r02gryyl4v8sqjn44xe.nsite.lol/docs/patterns/signer-login.md) is the implementation guide. This section holds only the *state of this machine*, as audited 2026-09-19. **Relays.** - `wss://relay.nsec.app` and `wss://relay.nostr.band` were unreachable from here (TLS/WS connect times out). Both are still hard-coded as defaults in several projects and libraries. - `auth.nostr1.com` demands NIP-42 AUTH before accepting a 24133. - `relay.nos.social` blocks kind 24133. - The local ngit-relay (`:8081`) and nostr-rs-relay (`:8080`) are unsuitable for NIP-46 tests. - Working set: see guide §3.6. **Code that looks like a reference but is still wrong. Do not copy it without the fix.** Found by reading; none of it has been fixed. | Where | Bug | Guide § | |---|---|---| | Every Go app on **stock** `fiatjaf.com/nostr` nip46/keyer (CFR, icdib, Starcraft Replaystr, nostrkit, Nostr-Music-Sampler V1–V3, In-Your-Face) | Waits for EOSE from all relays; drops NIP-04 replies; 30 s hard-coded sign timeout; broken `switch_relays`; waits for the relay OK before reading replies | 3.7 | | `In-Your-Face-try2/internal/identity/nostrconnect.go:110`, `cmd/iyf-web/main.go:299`; `I can do it better/cmd/icdib/api.go:216`, `main.go:202` | Bunker session built on a context that is cancelled when login returns, so the next sign fails with "context canceled" | 3.7 (Go lifetime rule) | | `nostrkit/identity/identity.go:131,159` (and CFR, icdib, Replaystr) | Raw login input wrapped with `%w`, so a mistyped nsec ends up in error text; `Redact` leaves `secret=` in bunker URIs | 2.6 | | `OTSuite-mobile` `Navigation.kt:297`, `NostrSigner.kt:117` | NIP-55 rejection (`RESULT_OK`+`rejected`) treated as success with an empty signature; missing column crashes instead of falling back | 4.3–4.5 | | `Idle StarFighter/web/shell.js:808` | NIP-55 web: rebuilds the event with a new `created_at` after the callback, so the signature is invalid | 5.3 | | `Nostr-Agenda` `SessionManager.kt:206` | NIP-46 `PERMS` contains bare `sign_event`, which Amber drops (the test only passed against nak) | 2.3 | | `Desktop/Archon-Web/src/bunker.js:313` | nostrconnect accepts a bare `"ack"` (spoofable) | 3.4.5 | | MKStack / `@nostrify/react` 0.50.2 `NUser.fromBunkerLogin` (EarthTreeMedia) | Restores with the user pubkey as the signer pubkey | 1 | | `work/nostr-archive-app` `Publish.tsx:279`, ETM `LoginDialog.tsx:173`, grimoire `LoginDialog.tsx:393` | NIP-07 availability decided at render time | 6.1 | | `Sessions/GRASPCONTROL` `manager.go:312` | QR flow can record the signer pubkey as the user when `get_public_key` times out; cancel doesn't stop the listener | 1, 3.4 | | `Facebook-Museum/app/third_party/nostr/nip46/client.go:28` (`ConnectPerms`, used at `:116`, `signer/nostrconnect.go:77`) | Bare `get_public_key,sign_event` perms (Amber strips them); client key kept in session memory only (D44), so a reload makes the client a stranger again | 2.3, 3.3.2 | | `Testimony/internal/signer/nip07.go` `SignEvent`; `third_party/nostr/FORK.md` "Known divergence" | Returned pubkey not compared with the session; the FORK note is stale (`portable.go:202` does send perms) | 2.1 | | Stock `fiatjaf.com/nostr` `wellknownnostrjson.go` | NIP-05 bunker lookup returns an unassigned named return, so login targets the zero pubkey and looks like "signer never answered" (fixed as Testimony Delta 10) | 3.7 | | `Sessions/Musig2_main/Archon-Web_v2/src/lib/core/identity/bunker-signer.ts:252` | Random client key per instance; nip44-only decrypt | 3.3.2, 3.5 | | `Sessions/bunker-android` `Bunker.kt:58` (signer side) | `connect` acks without checking the secret; incoming signatures aren't verified | — | **Secret leak to clean up.** `EarthTreeMedia-Nostr` `rip/blossom-upload.log`, `rip/media-map.json`, `compare/new-assets.log` and `compare/new-assets-map.json` contain a full `bunker://…secret=` URI and the client-key hex. A timed-out `nak` call echoed its command line into the logs. Scrub the files, and consider revoking that client in Amber. **Discovered by.** Cross-project signer audit, 2026-09-19. ## 7. nsite publishing: the trap set, measured **Read first:** [`patterns/nsite-publishing.md`](https://npub1d70emggs6jzun5lhvqnfqsd9reqmaarn2qjf6q3r02gryyl4v8sqjn44xe.nsite.lol/docs/patterns/nsite-publishing.md) is the implementation guide. This section holds the machine-state findings that made five separate agents each rediscover the same things. ### 7.1 Two `nak` binaries, and the one on `PATH` had no `nsite` command — RESOLVED `nak` has an `nsite` subcommand — `nak nsite upload|download`, a complete NIP-5A deploy in one call. Until 2026-09-21 you could not reach it: | Binary | Version | `nsite`? | On `PATH`? | |---|---|---|---| | `~/.local/bin/nak` | **v0.18.3** (Feb 2026) | **no** | **yes, won** | | `~/go/bin/nak` | build from source | **yes** | shadowed | So `nak nsite` looked like it did not exist, and four projects (Idle StarFighter → Nostr Workshop Board → Signer Login Tester → Cerebrate) each hand-rolled a ~130-line Python deploy script instead. **Resolved.** `nak` is now **v0.20.7** at `~/go/bin/nak` and is the only one on `PATH`; the old binary is parked at `~/.local/bin/nak.v0.18.3.bak`. Two traps survive for whoever reinstalls it: - **`nak --version` prints `nak version debug`** regardless of the version. The release ldflags are not set by `go install`, so the version string is a lie. The truth is `go version -m $(command -v nak) | grep fiatjaf/nak`. - **The vanity import path is broken.** `go install fiatjaf.com/nak@latest` fails — `fiatjaf.com/nak?go-get=1` advertises `git@github.com:fiatjaf/nak.git`, an scp-style SSH URL the Go tool rejects ("first path segment in URL cannot contain colon"). Install it as **`go install github.com/fiatjaf/nak@latest`**. (`fiatjaf.com/nostr`, the library, resolves fine — only `nak` is misconfigured.) ### 7.2 `blossom.primal.net` rewrites every `text/*` blob to `text/plain` The nsite gateway serves each file with the `Content-Type` the Blossom server recorded **at upload, keyed by sha256** — so a wrong type is only fixable by changing the file's bytes. Measured 2026-09-21, same three files, same uploader, fresh key: | Server | `.css` | `.js` | `.wasm` | GET | |---|---|---|---|---| | `https://nostr.download` | `text/css; charset=utf-8` | `text/javascript; charset=utf-8` | `application/wasm` | 200 | | `https://blossom.primal.net` | **`text/plain`** | **`text/plain`** | `application/wasm` | 302 → 200 | | `https://blossom.band` | upload **400** | upload **400** | upload **415** | 404 | | `https://azzamo.media` | upload **415** | upload **415** | upload **415** | — | | `http://localhost:8081` | `text/css; charset=utf-8` | `text/javascript; charset=utf-8` | `application/wasm` | 200 | **This corrects the folklore.** `nak blossom upload` was never the problem — it sends the correct type from the file extension (Go's `mime.TypeByExtension`), and both the local server and `nostr.download` honour it. `blossom.primal.net` is not "unreliable" and is not content-sniffing: it deterministically flattens `text/*` to `text/plain` and passes `application/*` through. A browser refuses a stylesheet served as `text/plain` and refuses to register a service worker served as `text/plain` — the page renders, completely unstyled, looking fine. `blossom.band` now rejects fresh-key uploads outright (400 on text, 415 on wasm), not just `.wasm` as previously recorded. Don't list it. `cdn.satellite.earth` is still 500 on everything. `inner.sebastix.social` is reachable again (its API is at the **root** path; `…/blossom/` is an HTML catch-all that 200s for anything and false-positives every dedup check). **`azzamo.media` is media-only and cannot host an nsite.** It is the owner's server (first in their kind 10063, published by Amethyst) and it accepts uploads from *any* key — the Blossom side is not gated the way `relay.azzamo.net` is (`payment_required` + `restricted_writes`). But it refuses by MIME with HTTP 415: `text/html` and `text/javascript` "not allowed for security reasons"; `text/css`, `text/plain` and `application/wasm` "not supported". Only `image/*` and friends get in, faithfully typed. Right server for media and for the side channel's file drops; never a `server` tag on a 15128. Canonical list: `configs/blossom.json`. **Live defect in the owner's 10063**: its second entry is `cdn.satellite.earth/`, which 500s. Their phone's media has a dead fallback. Fix from Amethyst — never with `nsyte --publish-server-list`, which would replace the whole list. **Workaround.** `nostr.download` first in the `server` list and abort if it fails; primal second, as an availability mirror only. Inlining CSS/JS remains good belt-and-braces, but it is a mitigation for server #2, not for `nak`. ### 7.3 Four divergent copies of the same deploy script Each project copied the last one and drifted. As of 2026-09-21: | | Idle StarFighter | Workshop Board | Signer Login Tester | Cerebrate | |---|---|---|---|---| | description tag | `description` | `description` | `description` | **`summary`** (not a spec tag) | | non-spec `relay` tags | yes | yes | yes | no | | aggregate `x` tag | no | no | no | no | | `/404.html` | no | no | no | no | | SPA route aliases | no | no | no | no | | gateway verification | no | no | no | no | | abort rule | all servers failed | all servers failed | all servers failed | first server failed | All four sites are live, because the bugs are latent: primal is listed second, so the wrong MIME only bites when `nostr.download` misses a blob; and none of the sites has deep links. **Fixed at source:** `bin/nsite-deploy.py` is now the shared implementation and does all six columns correctly. Point new projects at it instead of copying a fifth time. The four existing scripts were deliberately left alone — retrofitting them means redeploying four live sites. ### 7.4 A root site event with a `d` tag reports partial success and 404s `relay.nsite.lol` rejects it outright (`"Root site events must not include a 'd' tag"`); every other relay accepts it happily. So a broken deploy prints "2/4 relays accepted" and looks survivable, while the gateway can never find the site. A `d` tag belongs to kind 35128. Found 2026-09-21 publishing Cerebrate. More generally: **counting relay OKs is not a test.** `bin/nsite-deploy.py` fetches the gateway and checks the served content types before it claims success. **Discovered by.** nsite cross-project audit, 2026-09-21. ## How to log a new issue here Format: a section like the above with **Symptom / Cause / Affects / Workaround / Discovered by / Upstream**. Workspace-relevant only — issues that belong upstream and don't affect this stack go straight to the upstream tracker, not here. === FILE: https://npub1d70emggs6jzun5lhvqnfqsd9reqmaarn2qjf6q3r02gryyl4v8sqjn44xe.nsite.lol/docs/patterns/signer-login.md === > **Nostr Agent Onboarding** · [start here](https://npub1d70emggs6jzun5lhvqnfqsd9reqmaarn2qjf6q3r02gryyl4v8sqjn44xe.nsite.lol/start.md) · [index](https://npub1d70emggs6jzun5lhvqnfqsd9reqmaarn2qjf6q3r02gryyl4v8sqjn44xe.nsite.lol/llms.txt) · source: `nostr-dev/docs/patterns/signer-login.md` · snapshot 2026-10-10 > > Paths such as `~/Documents/…`, `~/Production Environment/…`, `repos/…` and services on `localhost` refer to the author's workstation and are **not available to you** — read them as worked examples of a setup you can recreate. # Pattern: signer login — NIP-07, NIP-46 (bunker / nostrconnect), NIP-55 (Amber) > **Read this before writing any login code.** Every app built on this machine > got signer login working only after several broken attempts (Facebook-Museum, > Testimony, Kubo, In-Your-Face, GRASPCONTROL, Idle StarFighter, Nostr-Agenda, > OTSuite …). The failures were almost always the same dozen mistakes. They are > collected here with the fix, so the next app gets it right in one go. > > Verified against the NIP texts on `nostr-protocol/nips` master, Amber source, > and the library sources on **2026-09-19**. Spec dates are in §9. If you are > reading this much later, re-check §9 first — NIP-46 and NIP-55 both changed > in 2026. > > **Test with the Signer Login Tester** (§8, ): a static page that exercises every > method below and logs every wire message. If your signer works there and not > in your app, the bug is in your app. --- ## 0. Which methods to offer | Platform | Offer | Notes | |---|---|---| | Web app / nsite | **NIP-07** + **NIP-46 paste `bunker://`** + **NIP-46 QR `nostrconnect://`** | NIP-55-over-web is optional and fragile (§5). The NIP-55 spec itself says web apps should prefer NIP-46. | | Android native | **NIP-55** (Amber etc.) + **NIP-46** | Use Quartz's `nip55AndroidSigner`, don't hand-roll (§4). | | Go desktop / daemon / CLI | **NIP-46** | Use the patched library from Testimony, not stock `fiatjaf.com/nostr` (§3.7). | | Go→wasm in the page | NIP-07 via a JS bridge + NIP-46 | Extensions don't inject `window.nostr` into workers — bridge from the page (Facebook-Museum `cmd/wasm/extsigner.go`). | Web clients must never ask for an nsec (Nostr-exp [`core/03-how-to-develop.md`](https://npub1d70emggs6jzun5lhvqnfqsd9reqmaarn2qjf6q3r02gryyl4v8sqjn44xe.nsite.lol/docs/core/03-how-to-develop.md)). Local-key onboarding is acceptable only in native introductory apps. --- ## 1. The three keys (the #1 conceptual bug) NIP-46 has three keypairs. Mixing them up causes most "logged in as the wrong person" and "can't reconnect" bugs: | Key | Who holds it | Where you see it | |---|---|---| | **client key** | your app, generated once, **persisted** | host part of `nostrconnect://` | | **remote-signer key** | the signer | host part of `bunker://`; author of every response | | **user key** | the signer | only via `get_public_key` | - The remote-signer key **may** equal the user key (Amber) but usually doesn't (nsec.app, nsecbunker, nak with separate keys). **Always call `get_public_key` after `connect`** and use its result as the session pubkey. - Persist all three plus the relays. Example bug: nostrify 0.50.2 `NUser.fromBunkerLogin` restores with the *user* pubkey as the signer pubkey. It works with Amber and times out with everything else. --- ## 2. Rules that apply to every method 1. **Verify every signed event you get back.** Check that `verifyEvent(ev)` is true, that `ev.pubkey === sessionPubkey`, and that `kind`, `content`, `tags` and `created_at` equal what you asked for. Recompute the id yourself. Extensions can switch accounts under you; bunkers can return anything. 2. **Every request has a timeout, and every timeout is visible in the UI.** Humans approve on phones, so use about 120 s for connect and QR pairing and about 60 s per sign. Libraries default to *no* timeout: nostr-tools `sendRequest`, NDK `signer.timeout`, and applesauce all wait forever. 3. **Kind-scoped permissions.** Request `sign_event:` for each kind you will sign. Amber silently discards bare `sign_event` (`removeIf { kind == null && type == "sign_event" }`), for both NIP-46 and NIP-55. 4. **Log the wire.** Keep a debug view of each request, response, relay OK and error. Every multi-day debugging session here ended once someone looked at the raw 24133 traffic. 5. **Say the Amber caveats in the UI:** - Amber drops *all* incoming NIP-46 requests when its Android notification permission is off (the Android 13+ default). - Since Amber 6.5.0 it silently rejects requests whose `created_at` is more than 5 minutes off its clock. 6. **Never print the secret.** `bunker://…&secret=` and client keys have leaked into logs and error strings here several times, for example EarthTreeMedia `rip/*.log` and nostrkit `Redact`. Redact `secret=` before logging a URI, and don't wrap raw user input into errors with `%w`. --- ## 3. NIP-46 — remote signing (bunker and nostrconnect) ### 3.1 Wire format (current) - Kind **24133** (ephemeral). `content` = **NIP-44** ciphertext of JSON. Requests are p-tagged with the remote-signer pubkey; responses are p-tagged with the client pubkey. - Request: `{"id": "", "method": "", "params": ["", …]}`. All params are strings. - Response: `{"id": "", "result": "", "error"?: ""}`. If `error` is present, the request failed. `sign_event`'s result is a JSON *string*, not an object. | method | params | result | |---|---|---| | `connect` | `[remote_signer_pubkey, secret_or_"", perms_or_"", client_metadata_json]` | `"ack"` or the secret | | `get_public_key` | `[]` | user pubkey, hex | | `sign_event` | `[json({kind, content, tags, created_at})]` | `json(signed event)` | | `nip44_encrypt` / `nip44_decrypt` / `nip04_encrypt` / `nip04_decrypt` | `[third_party_pubkey, text]` | text | | `ping` | `[]` | `"pong"` | | `switch_relays` | `[]` | JSON string `["wss://…"]` or `null` | | `logout` | `[]` | `"ack"` (courtesy; you must still delete the client key) | - `client_metadata_json` is `{"name":…,"url":…,"image":…}`. It was added 2026-06-23. To send metadata without perms, pass `""` in the perms slot. - **auth challenge:** a response of `{"id", "result":"auth_url", "error":""}` means "open this URL". Keep listening for a **second response with the same id**, which is the real result. ### 3.2 URIs **bunker://** (the signer creates it; the user pastes it into your app): ``` bunker://?relay=wss://a&relay=wss://b&secret= ``` **nostrconnect://** (your app creates it; shown as a QR, or as a link on the same phone): ``` nostrconnect://?relay=wss%3A%2F%2Fa&relay=wss%3A%2F%2Fb&secret=&perms=sign_event%3A1%2Cnip44_encrypt&name=MyApp&url=https%3A%2F%2Fmyapp.example ``` - Both `relay` and `secret` are required. - `metadata=` is the **old** form (before 2024-11). Use `name`/`url`/`image`. - **Amber URL-decodes the whole query and then splits on `&` and `=`.** Any value containing `&`, `=`, `+` or a space breaks even when percent-encoded. So keep `name` to one word, make `secret` hex, and leave out `image` unless its URL is simple. - Use a hex secret from a CSPRNG (`crypto.getRandomValues`), never `Math.random`. ### 3.3 Checklist: the bunker:// flow (paste) 1. Parse the URI: signer pubkey plus relays plus secret. Tolerate a missing secret. 2. **Load the persisted client key, or create one and save it now,** before the first `connect`. - Amber auto-acks a repeat connect from a client it already knows. - A fresh key per attempt looks like a stranger reusing a consumed single-use secret, and **no prompt ever appears**. This is the #1 cause of "Amber doesn't work". 3. Connect to the relays and **drop the dead ones**. Relay health changes daily; see §3.6. 4. Open the reply subscription first: `{"kinds":[24133], "#p":[client_pubkey], "since": now-60}` with **no `limit`**. - Don't use `since: now`: Amber stamps replies about 5 s in the future. - Don't use `limit: 0` alone: that relies on relays live-streaming correctly. - Don't wait for EOSE from *all* relays before sending. One dead relay then hangs login forever; that is a stock `fiatjaf.com/nostr` and NDK bug. Wait at most about 2 s for the first EOSE, then send. - Don't send before the REQ is live either. Under wasm, the request beat the REQ on a shared socket and the ephemeral reply was never replayed. 5. **Always send `connect`**, even without a secret: `[signerPk, secret||"", perms||"", metadataJSON]`. Some signers refuse everything from a client that never connected. 6. Accept `"ack"` **or** the secret as a successful connect result. 7. Call `get_public_key`. That is the user. 8. Send `switch_relays`. If it returns a list, move the session to those relays (resubscribe first, then drop the old ones). This has been a spec SHOULD since 2026-01-26. Amber 6.3+ changed its default relays, so this matters. 9. Persist `{clientSecret, signerPubkey, userPubkey, relays}`. Reconnect later with `connect` without the secret (the secret was single-use). 10. On logout: send `logout` as a courtesy, then **delete the client key**. ### 3.4 Checklist: the nostrconnect:// flow (QR) 1. Create the client key and persist it. Create a hex secret. 2. Choose relays the signer is likely to use (§3.6). **Open and arm the subscription before showing the QR.** A fast phone replies within a second; if you're not listening yet, the reply is lost, because 24133 is ephemeral. 3. Show the QR, a copy button, and a plain `nostrconnect://` link. On the same phone, tapping the link opens Amber directly. 4. Accept **either** of these as the pairing reply: - a *response* whose `result === secret` (what current signers send), or - a legacy `connect` *request* whose `params[1] === secret`. 5. **Never accept a bare `"ack"` in this flow.** The client pubkey is printed in the QR, so anyone can reply `"ack"` and become "your signer". The spec says the client MUST validate the secret. nostr-tools enforces this since 2026-08-19. applesauce, rust-nostr, nostr-login and Archon-Web still accept `"ack"`, so don't copy them. 6. The **author of the pairing reply is the remote-signer pubkey**. Then continue with §3.3 steps 7–10. ### 3.5 Encryption dialect - **Send NIP-44 only.** The spec dropped NIP-04 for the 24133 envelope on 2024-12-05. - **Decrypt with NIP-44 first, then fall back to NIP-04** (the ciphertext contains `?iv=`). Old signers still reply in NIP-04, and Amber still accepts both. - **Never dual-send** (the same request in both encryptions). Amber dedupes by event id and consumes the single-use secret on the first copy. The second copy then errors and **no prompt appears**. - NIP-44 was extended on 2026-06-28 to allow plaintexts over 64 KiB, using a 6-byte length prefix. Old decoders, including paulmillr's reference `nip44`, reject those. Large `sign_event` payloads can hit this. ### 3.6 Relays for NIP-46 (probed 2026-09-19, fresh keys, publish and receive) | Relay | 24133 relayed? | Note | |---|---|---| | `wss://relay.nip46.com` | ✅ | dedicated NIP-46 relay; in Amber 6.6 defaults | | `wss://bucket.coracle.social` | ✅ | in Amber 6.6 defaults | | `wss://nrs.primal.net` | ✅ | in Amber 6.6 defaults | | `wss://relay.damus.io` | ✅ | **removed** from Amber defaults in 6.3; intermittent 503s | | `wss://relay.primal.net`, `wss://nostr.oxtr.dev` | ✅ | | | `wss://auth.nostr1.com` | ⚠️ AUTH required | in Amber defaults; you must answer NIP-42 AUTH (sign kind 22242 with the **client key**) or publishes fail with `auth-required` | | `wss://relay.nsec.app` | ❌ unreachable | still the default in many libraries and in apps on this machine; don't rely on it | | `wss://relay.nos.social` | ❌ `blocked: kind not allowed` | | | `wss://nos.lol` | ❌ timed out that day | | | local ngit-relay `ws://localhost:8081`, nostr-rs-relay | ❌ / never OKs | don't use for NIP-46 tests; use a public relay or an in-process khatru | - **Default for new apps:** `relay.nip46.com` + `bucket.coracle.social` + `nrs.primal.net` + `relay.damus.io`. - Let the user edit the list, and probe it at login. - Re-probe with the tester (§8) or with this, from a script file (nak stalls in inline compound shell): `nak req -k 24133 -p --stream wss://R` plus `nak event -k 24133 -p -c x --sec wss://R`. - **Don't gate on relay OK** for 24133. Some relays never OK ephemeral events. Start waiting for the reply immediately. ### 3.7 Library status (2026-09-19): what to use and what to patch | Library | Verdict | Bugs you must work around | |---|---|---| | **nostr-tools** `BunkerSigner` (≥2.25) | best JS base | No per-request timeout (wrap it). A reply with an empty `result` never resolves. `switch_relays` compares a sorted list against an unsorted one. `queryBunkerProfile` reads the old NIP-05 `nip46` shape. Pass a persisted `clientSecretKey`. | | **applesauce-signers** `NostrConnectSigner` | good (grimoire uses it) | Accepts bare `"ack"` in nostrconnect. `switchRelays` checks `Array.isArray` on a string, so it never switches. No timeout. `onAuth` defaults to `window.open` outside a click, which popup blockers stop. | | **NDK** `NDKNip46Signer` (3.0.x) | avoid for NIP-46 | Sends `connect` with `[userPubkey??"", secret]` (the wrong first param). Only `"ack"` counts as success. Waits for EOSE before sending. Sticks to NIP-04 after seeing one NIP-04 reply. The nostrconnect URI has a single relay and a `Math.random` secret. No timeout by default. | | **nostrify** (MKStack) | ok for NIP-07; bunker restore buggy | Bug in §1. No `auth_url` handling. | | **fiatjaf.com/nostr** `nip46` (Go, stock) | **don't use stock** | Waits for EOSE from all relays (a dead relay hangs forever). Drops NIP-04 replies. 30 s hard-coded sign timeout (`BunkerSignTimeout` is ignored). One context bounds both the connect and the listener (the second sign fails with "context canceled"). Broken 1-in-10 pre-connect `switch_relays`. Waits for the relay OK before reading the reply. **Use the patched copy in `Production Environment/Testimony/third_party/nostr/` (see its `FORK.md`) plus `internal/signer/`.** | | **rust-nostr** `nostr-connect` | ok | Accepts `"ack"` in nostrconnect. No `switch_relays`/`logout`. | | **nostr-login** | unmaintained since 2025-03 | Don't adopt. | **Go lifetime rule.** The bunker client's subscription lives as long as the context you created it with. Build it with an **app-lifetime context** and use per-request `context.WithTimeout` only around individual calls. In-Your-Face-try2 and icdib both `defer cancel()` the login context, so the session dies the moment login returns. --- ## 4. NIP-55 — Android native (Amber) Use **Quartz** (`com.vitorpamplona.quartz`, `nip55AndroidSigner`), as Nostr-Agenda does. It already does content-resolver-first with intent fallback, id matching, rejection handling, signature verification and timeouts. If you must hand-roll, implement all of this: 1. **Manifest ``**, or Android 11+ can't see the signer (neither the intent nor the ContentResolver): ```xml ``` Detect with `queryIntentActivities(Intent(ACTION_VIEW, Uri.parse("nostrsigner:")))`. Show "Sign in with Amber" only if something is installed. 2. **Login = one `get_public_key` intent** with *no* package set, plus a `permissions` extra that lists every kind you'll sign: `[{"type":"sign_event","kind":31922},{"type":"nip44_decrypt"}]`. - Quartz's default covers only kind 22242, which means a prompt on every sign. - Store `result` (the pubkey) **and `package`**. Accept both npub and hex. - Don't call `get_public_key` again while logged in. 3. **Later requests:** - First try the ContentResolver: `content://.SIGN_EVENT`, with `selectionArgs = [payload, pubkey_hex, current_user_hex]`. - A `null` cursor or empty row means manual approval is needed, so fall back to the intent with `setPackage(package)`. - A `rejected` column means the user chose "always reject". **Stop; don't fall back.** - Use `getColumnIndex` safely: a missing column must not crash. 4. **Read `result`**, not `signature`. Amber sets both; other signers may not. `event` is also returned for `sign_event`. 5. **Rejection is `RESULT_OK` + `rejected=true`** (spec rewrite 2026-06-03). `resultCode != RESULT_OK` means the signer failed. Code that only checks `resultCode` turns a rejection into "success with an empty signature" (the OTSuite bug). 6. Match results by `id`. Queue requests: one Amber prompt at a time (OTSuite `SignerIntentBridge` mutex). Deliver a cancel to the waiting caller; don't let it time out silently. 7. **Compose pitfall.** Activity results arrive while the screen is STARTED, not RESUMED, so navigating directly from the callback is silently dropped. Set a flag and navigate in a `LaunchedEffect`. --- ## 5. NIP-55 over the web (`nostrsigner:` URLs), optional and fragile The spec recommends NIP-46 for web apps. If you still offer it (it's the only no-relay option for a phone browser): ``` nostrsigner:?compressionType=none&returnType=signature&type=&callbackUrl= ``` 1. **Callback must be fragment-based**: `callbackUrl=https://host/path#nip55=`. - Amber splits the decoded data on `?`, so a `?x=` callback gets mangled (Idle StarFighter `d98801c`). - Amber opens `callbackUrl + Uri.encode(result)` in a new VIEW intent. - Listen for **`hashchange`** as well as page load, because a fragment-only navigation doesn't reload the page. 2. **Pending state in `localStorage`** with an expiry (not `sessionStorage`). The result often lands in a *new tab*. 3. **Request `returnType=signature`**. Rebuild the event from the **exact stored unsigned event**, including its `created_at`, recompute the id, then verify. - Rebuilding with a new `created_at` breaks the signature. This is the Idle StarFighter bug. - For `sign_event`, include `pubkey` in the unsigned JSON. - Encode the payload with `encodeURIComponent`. An unescaped `?` inside the JSON breaks Amber's parser. 4. **With no `callbackUrl`**, Amber copies the result to the clipboard and shows a toast. Offer a "paste result" box as the fallback. 5. **Chrome (reported, not independently confirmed).** - Since about August 2026, Chrome reportedly no longer sends `EXTRA_APPLICATION_ID`, so Amber treats a bare `nostrsigner:` link as an app intent and fails with "malformed nostrsigner request". - Amber's app-intent branch reads `type`, `callbackUrl`, `returnType`, `pubkey` and `id` from **extras** (verified in Amber source, `IntentUtils.getIntentDataFromIntent`). So an Android `intent:` URL works as a fallback: `intent:#Intent;scheme=nostrsigner;S.type=sign_event;S.returnType=signature;S.callbackUrl=;end`. - Offer both links. 6. A `nostrsigner:` link with no signer installed does nothing, and this can't be detected. Say so in the UI. 7. When an installed PWA opens the callback, it opens in a browser tab, not the PWA window. --- ## 6. NIP-07 — browser extension ``` await window.nostr.getPublicKey() // hex await window.nostr.signEvent({created_at, kind, tags, content}) // → full event window.nostr.nip44?.encrypt/decrypt(pk, text) // optional — feature-detect window.nostr.nip04?.encrypt/decrypt(pk, text) // deprecated ``` 1. **Injection is late.** Extensions inject after your script runs (nos2x adds an async `