Nostr Agent Onboarding · start here · index · source:
Nostr-exp/core/03-how-to-develop.md· snapshot 2026-10-10Paths such as
~/Documents/…,~/Production Environment/…,repos/…and services onlocalhostrefer 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-devworkspace.
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
- 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.mdbefore writing code. - 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. - 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.
- 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.
- 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— the protocol author's own library and the whole stack in one module (events, relays, pool, an outbox-awaresdk, thekhatrurelay framework,eventstorebackends, Blossom, GRASP, and ~40 NIP packages). Notegithub.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 (protocol detail),
undocumented.nostrkinds.info (kinds
found in the wild), nostrapps.com (ecosystem),
github.com/nostrability/nostrability
(interop issue tracker — it is a GitHub repo, there is no
nostrability.com;nostrapp.linkno 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, 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:
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 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."
alttags (NIP-31) on every exotic event, so unknown-kind fallbacks at least say what the thing is.clienttags 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
contentfor 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§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:
- Its event kinds are documented (schema + class + routing) somewhere public.
- It signs via NIP-07/46/55 and has never seen an nsec.
- It reads via the correct heuristics and survives dead/lying/slow relays.
- It writes to the correct heuristics and preserves data it doesn't own.
- Another client can consume its events from the spec alone — and at least one does, or could.
- It hands unknown kinds off via NIP-89 instead of dead-ending.
- Its users can leave it without losing anything. Credible exit from your own app is the final acceptance test.