> **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 <kind> -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.**
