Nostr, Explained for Developers — A Deep Dive

Nostr, Explained for Developers — A Deep Dive

# nostr# bitcoin# opensource
Nostr, Explained for Developers — A Deep DiveAmayo Clinton

If you've spent any time in the Bitcoin or decentralized-social corners of the internet lately,...

If you've spent any time in the Bitcoin or decentralized-social corners of the internet
lately, you've probably seen the word Nostr thrown around next to terms like "zaps,"
"relays," "npub," and "NIP-whatever." Most explainers stop at the surface — "it's
decentralized Twitter." That's true, but it undersells what's actually going on and
skips the parts that matter if you're a developer deciding whether to build on it.

This post goes deeper: the protocol's actual wire format, why it was designed the way it
was, how it compares to the alternatives that came before it, what real production
systems look like on top of it, and where the sharp edges are. By the end you should be
able to read the spec yourself and know exactly where to start building.

Table of contents

  1. What Nostr actually is and isn't
  2. The problem it was designed to solve
  3. The protocol, piece by piece: keys, events, relays, clients
  4. How the wire protocol actually works (REQ / EVENT / CLOSE)
  5. NIPs: the extension system that lets the protocol grow
  6. Zaps: the feature that makes Nostr different from everything before it
  7. Nostr vs. ActivityPub vs. Bluesky's AT Protocol
  8. What real production apps look like on this stack
  9. The honest tradeoffs and open problems
  10. Getting started: a minimal working example
  11. Where this is heading

1. What Nostr actually is and isn't

Nostr stands for Notes and Other Stuff Transmitted by Relays. It
was first sketched out by a pseudonymous developer known as fiatjaf in late 2020, with the
explicit goal of being "the simplest possible thing that works" — a deliberate reaction
to how complex and heavyweight ActivityPub (the protocol behind Mastodon) had become.

What it is: an open protocol specification — not a company, not a single app, not a
blockchain, not a token. There's no Nostr Inc. to shut down and no central server to
subpoena, because there isn't one. It's closer in spirit to email (SMTP) or the web
(HTTP) than to a product — a shared format that many independent implementations agree
to speak.

What it isn't: a blockchain. This trips people up constantly because of how tightly it's
associated with Bitcoin. There's no consensus mechanism, no mining, no global ledger of
truth. Events aren't "mined" — they're just signed and broadcast. The only place Bitcoin
actually enters the picture is optional: the Lightning Network is used for in-protocol
payments (zaps), and secp256k1 — the same elliptic curve Bitcoin uses for keys — happens
to be the curve Nostr identities are built on, mostly because it's fast, well-audited,
and developers building in this space already had tooling for it.

2. The problem it was designed to solve

Every centralized social platform, no matter how well-intentioned, has the same structural
weakness: your identity, your social graph, and your content all live inside one
company's database, under one company's rules. If that company suspends you, changes its
algorithm, sells to a new owner, or shuts down entirely, everything you built there is
gone — not because you did anything wrong, but because you never actually owned any of
it. You were a tenant.

Mastodon and the wider Fediverse tried to fix this with federation — instead of one
company, thousands of independently run servers, each running the ActivityPub protocol,
talking to each other. This is a real improvement, but it only partially solves the
problem: your identity is still tied to whichever server you signed up on
(@you@mastodon.social). If that specific server's admin bans you, or the server shuts
down, you lose your handle, your followers, and your post history, even though the wider
network survives. You've traded one company for one admin.

Nostr's answer is to go one layer deeper: separate identity from hosting
entirely, so that neither a company nor a single server admin can hold your identity
hostage.

3. The protocol, piece by piece

3.1 Identity is a keypair, not an account

There is no sign-up flow on Nostr in the traditional sense. "Creating an account" means
generating a standard secp256k1 keypair, client-side, with no server involved at all:

  • nsec (private key) — the thing that lets you sign events. Never uploaded anywhere in plaintext, never sent to a server, ever.
  • npub (public key) — your identity, in a human-shareable, Bech32-encoded format. This is you, across every Nostr app that exists, forever, unless you choose to generate a new one.

Because no company or server issues this identity, none of them can revoke it. If you
stop trusting one client, you install a different one, log in with the same key, and your
followers, your post history, and your profile are all instantly there — not because
they were "migrated," but because they were never tied to that app in the first place.
This is structurally impossible to replicate on Twitter, Instagram, or Discord, where
your identity is a row in a database you don't have access to.

3.2 Content is a signed event — and that's the entire data model

This is the part that genuinely surprises people the first time they read the spec:
there is exactly one data structure in Nostr. A post, a profile update, a like, a direct
message, a long-form article, a Lightning payment receipt — all of them are the same
JSON shape:

{
  "id": "5c83da777af...",
  "pubkey": "91cf9...f857",
  "created_at": 1721390400,
  "kind": 1,
  "tags": [
    ["p", "some-other-pubkey-being-replied-to"],
    ["e", "id-of-the-note-being-replied-to"]
  ],
  "content": "hello, nostr",
  "sig": "908a15e4f8de..."
}
Enter fullscreen mode Exit fullscreen mode

Breaking that down:

  • id — a SHA-256 hash of the serialized event data. This means the ID isn't assigned by a server; it's a deterministic fingerprint of the content itself. Change one character of the content and you get an entirely different ID.
  • pubkey — the author's public key, in hex.
  • created_at — a Unix timestamp, set by the client, not the server.
  • kind — an integer that tells every relay and client what type of event this is. This single field is what lets the protocol stay generic while supporting wildly different features — more on this below.
  • tags — an array of arrays, used for references, replies, mentions, hashtags, and metadata specific to that event kind. This is the protocol's extensibility escape hatch: instead of adding new top-level fields for every feature, features are expressed through tags.
  • content — the actual payload, usually a string (sometimes itself JSON, sometimes encrypted, depending on the kind).
  • sig — a Schnorr signature over the event ID, produced with the author's private key. Anyone, anywhere, can verify this signature using nothing but the public key and the event data — no server round-trip required.

That signature is the whole trust model. A relay could be hostile, compromised, or run by
someone with terrible intentions, and it still can't forge a post from you or alter
your existing posts without breaking the signature — any client that checks it will
immediately reject a tampered event.

3.3 Kinds: the field that does all the work

The kind number is genuinely the most important design decision in the whole protocol.
A few of the most widely used:

Kind Meaning
0 Profile metadata (display name, avatar URL, NIP-05, etc.)
1 Short text note — the "tweet"
3 Contact list — who you follow
4 Encrypted DM (legacy, deprecated — see NIP-17 below)
5 Deletion request
6 Repost
7 Reaction (like, or an arbitrary emoji/vote)
1063 File metadata
9734 / 9735 Zap request / zap receipt
30023 Long-form article
39000-39009 Group metadata (NIP-29)

Kinds are grouped into ranges with different storage semantics — some are "regular"
(every event is kept forever), some are "replaceable" (only the newest event per
pubkey+kind is retained, useful for things like profile metadata that shouldn't
accumulate history), and some are "ephemeral" (not stored at all, just relayed live,
useful for typing indicators or presence). This range-based convention means a relay can
implement sensible storage behavior for a brand-new kind it's never seen before, just by
checking which numeric range it falls into.

3.4 Relays are dumb pipes, not authorities

A relay is nothing more than a WebSocket server that stores events and answers queries
for them. That's the entire job description. Critically, a relay has no special power
over your identity — it can refuse to store your events (rate limiting, spam filtering,
an outright ban from that specific relay), but it cannot delete your account, because
there was never an account for it to control in the first place. Your identity lives in
your keypair, not in any relay's database.

In practice, clients publish the same event to several relays simultaneously — typically
somewhere between three and ten. If one relay goes offline permanently, or bans you, or
turns out to be run by someone acting in bad faith, your content and identity survive
intact on every other relay you published to. This redundancy is the actual mechanism
that makes the network censorship-resistant — not any single relay's goodwill, but the
fact that no single relay is a point of failure.

3.5 Clients are interchangeable, by construction

Damus, Primal, Amethyst, Ditto, YakiHonne, Iris, Coracle — these are all independently
built apps, often by teams that have never talked to each other, reading and writing the
exact same event format from the exact same relays. Switch from one to another mid-day
and your followers, your posts, your DMs, and your Lightning wallet connection all show
up automatically, because none of it was ever app-specific data — it's just events on
relays, addressed by your pubkey.

4. How the wire protocol actually works

It's worth seeing the raw mechanics once, because it demystifies a lot of what "Nostr
app" actually means under the hood. Everything happens over a WebSocket connection to a
relay, using a small set of message types.

Publishing an event — the client sends:

["EVENT", { ...the signed event object... }]
Enter fullscreen mode Exit fullscreen mode

The relay responds with an OK message confirming whether it accepted or rejected the
event (and why, if rejected — e.g. rate-limited, invalid signature, blocked pubkey).

Requesting events — the client sends a subscription request with a filter:

["REQ", "subscription-id-1", { "kinds": [1], "authors": ["91cf9...f857"], "limit": 20 }]
Enter fullscreen mode Exit fullscreen mode

The relay streams back matching events as EVENT messages tagged with that subscription
ID, followed by an EOSE ("end of stored events") message once it's sent everything it
currently has — after which the subscription stays open and the relay pushes any new
matching events live, in real time, as they arrive. This is how a Nostr feed actually
updates without polling: it's a long-lived subscription over an open socket.

Closing a subscription:

["CLOSE", "subscription-id-1"]
Enter fullscreen mode Exit fullscreen mode

That's genuinely most of the protocol's mechanical surface. Everything more advanced —
groups, zaps, encrypted DMs — is built by defining new event kinds and tag conventions on
top of this same three-message exchange, not by adding new wire-level message types.

5. NIPs: the extension system

The base spec, NIP-01, only defines the event format and the REQ/EVENT/CLOSE
exchange above. Everything else is an optional extension called a NIP — a "Nostr
Implementation Possibility," numbered and published in a public GitHub repository.
Clients and relays adopt whichever NIPs make sense for what they're building; nothing is
mandatory. This keeps the core protocol genuinely tiny — you can read NIP-01 in about ten
minutes — while letting the surrounding ecosystem grow features without ever breaking
backward compatibility, since old clients simply ignore kinds and tags they don't
recognize.

A few of the most consequential NIPs in production today:

  • NIP-05 — maps a human-readable identifier (formatted like an email address, e.g. alice@example.com) to a pubkey via a .well-known DNS lookup, so users see something more recognizable than a wall of hex characters, and so impersonation is at least partially mitigated.
  • NIP-17 / NIP-44 — properly encrypted, metadata-minimizing direct messages, using a "gift wrap" scheme that hides even who's talking to whom. This replaced the earlier NIP-04, which encrypted message content but left sender, recipient, and timing fully visible to any relay — a real privacy hole for anything called an "encrypted" DM.
  • NIP-23 — long-form content: real articles with titles, summaries, and Markdown bodies, rather than short notes.
  • NIP-25 — reactions: likes, and by extension arbitrary up/down-style voting, since the content field can hold any string, not just an emoji.
  • NIP-29 — relay-based groups: the closest thing Nostr has to a "server" in the Discord sense, with defined admin/moderator roles, membership lists, and channels, with permission checks enforced by the relay hosting the group rather than trusted from the client.
  • NIP-42 — relay authentication, needed for any access-gated or private content.
  • NIP-46 — remote signing (often called a "bunker"): lets an app request a signature from a separate signer application, so the app itself never has direct access to your raw private key.
  • NIP-57 — zaps, covered in detail next.

Anyone can draft a NIP. Adoption is entirely organic — a NIP "succeeds" not because a
governing body approves it, but because enough clients and relays independently decide
it's worth implementing, the same evolutionary pressure that determines which HTTP
headers or email extensions become universal over time.

6. Zaps: the feature that makes Nostr different from everything before it

ActivityPub solved federation over a decade ago. What it never solved — what no prior
open social protocol solved — is native monetization. There's no built-in way to pay a
creator inside ActivityPub itself; every attempt bolts on a separate payment processor,
subscription platform, or ad network sitting outside the protocol.

Nostr's answer, defined in NIP-57, is called a zap: a Lightning Network payment
wrapped inside a signed Nostr event and attached directly to the specific post or profile
it's paying. Mechanically, it works like this:

  1. You tap "zap" on a post. Your client constructs a kind: 9734 zap request event, referencing the post being zapped, and sends it to the recipient's configured Lightning address.
  2. That Lightning address returns a BOLT11 invoice.
  3. Your wallet pays the invoice over the Lightning Network.
  4. The recipient's Lightning service provider publishes a kind: 9735 zap receipt event back to the relays, cryptographically proving the payment happened and linking it to the original post.
  5. Every client that renders that post can now display the zap total, because the proof of payment lives in the same event graph as the content itself.

The significant part isn't the mechanism — it's that the payment and the social action
happen in the same protocol layer, with no ad network, no platform fee, and no separate
payment processor mediating the relationship between creator and audience. This is the
specific reason Bitcoin-native developers gravitate toward Nostr rather than other
decentralized-social efforts: the social graph and the payment rail are literally the
same underlying system, not two separate products stitched together.

7. Nostr vs. ActivityPub vs. Bluesky's AT Protocol

Three protocols regularly get compared, so it's worth being precise about how they differ:

Nostr ActivityPub (Mastodon) AT Protocol (Bluesky)
Identity Cryptographic keypair, held by the user Handle tied to a specific server (@you@server) DID (decentralized identifier), can be self-hosted or provider-hosted
Hosting model Any relay, freely swappable Federated servers, admin-controlled "Personal Data Servers," designed for portability
Moving providers Trivial — same key, any client/relay Difficult — handle and history are server-bound Supported by design, but less battle-tested
Native payments Yes — zaps (NIP-57), Lightning-native No No
Protocol complexity Deliberately minimal Comparatively heavyweight (inherited from ActivityStreams) Moderate, with a stronger emphasis on algorithmic feeds
Governance No formal body; NIPs adopted organically W3C standard, more formal process Steered primarily by Bluesky PBC

None of these is strictly "better" in the abstract — they optimize for different things.
ActivityPub optimizes for federation between admin-run communities. AT Protocol optimizes
for algorithmic feed flexibility with portability as a design goal. Nostr optimizes for
minimal trust surface and puts a payment rail directly into the protocol. Which one a
given project should build on depends entirely on which of those properties actually
matters for the problem being solved.

8. What real production apps look like on this stack

It helps to look at two projects that made very different architectural choices on top
of the exact same protocol:

Ditto is a self-hostable community server, written in Deno/TypeScript with Postgres
for indexing. Rather than building a new UI from scratch, it implements Mastodon's REST
API on top of Nostr events — so any existing Mastodon-compatible client can point at a
Ditto instance and just work, while the actual data underneath is portable Nostr events
rather than server-locked ActivityPub records. It's aimed squarely at admins who want to
run their own community without the deplatforming risk that comes with depending on one
company's infrastructure.

YakiHonne took the opposite approach: rather than being self-hosted infrastructure,
it's a polished client app (web, iOS, Android) with its own relay network built in
strfry (C++), optimized for long-form publishing and creator monetization, with a
built-in Lightning/Cashu wallet baked directly into the app. It also introduced Smart
Widgets
— interactive mini-apps encoded as Nostr events — pushing the protocol beyond
static posts into small embedded applications.

Both are legitimate, production-grade approaches to the same underlying protocol, which
is the point: Nostr isn't a single app you either use or don't, it's a substrate that
different teams build genuinely different products on top of — the same way SMTP
underlies both a self-hosted mail server and Gmail.

9. The honest tradeoffs and open problems

Decentralization isn't free, and a fair explainer has to say so plainly.

  • Discovery is unsolved. No company curates a global feed, which is exactly the point, but it also means good content discovery has to be engineered by each client independently, and most haven't fully cracked it yet.
  • Moderation is per-relay, not global. A relay can reject spam or abusive content on itself, but there's no way to remove something from the network entirely — it may still exist on other relays. This is a deliberate consequence of the trust model, and it means building real abuse-handling tooling is squarely the responsibility of anyone running a relay or client, not something you inherit for free.
  • Key management has no safety net. There's no "forgot password" flow. Lose your nsec and you've lost that identity permanently, unless the specific client you used built its own backup/recovery scheme on top of the base protocol.
  • No global consensus on state. Things like a reaction count or a vote total are only ever a view assembled from whichever relays your client happened to query — not a single canonical number the way a centralized database would give you. Two different clients can legitimately show two different totals for the same post.
  • Spam resistance is still maturing. Because publishing an event is nearly free, relays rely on rate limiting, proof-of-work stamps (NIP-13), paid relays, or allowlists to keep spam manageable — there's no single dominant solution yet.

None of these are protocol bugs exactly — they're the direct, unavoidable tradeoff for
not having a company sitting in the middle making those decisions on your behalf.
Building well on Nostr means designing around these constraints explicitly, rather than
assuming the platform-style guarantees a centralized backend would otherwise give you for
free.

10. Getting started: a minimal working example

If you want to see the protocol in action, here's the shortest path from zero to a
published event using nostr-tools (JavaScript/TypeScript):

import { generateSecretKey, getPublicKey, finalizeEvent } from 'nostr-tools/pure';
import { Relay } from 'nostr-tools/relay';

// 1. Generate a keypair (this is your "sign up")
const sk = generateSecretKey();
const pk = getPublicKey(sk);

// 2. Construct and sign an event
const event = finalizeEvent({
  kind: 1,
  created_at: Math.floor(Date.now() / 1000),
  tags: [],
  content: 'hello, nostr',
}, sk);

// 3. Publish it to a relay
const relay = await Relay.connect('wss://relay.damus.io');
await relay.publish(event);
console.log('Published as', pk);
Enter fullscreen mode Exit fullscreen mode

That's a complete, working Nostr client in about a dozen lines — no server, no signup
form, no API key. For anything user-facing, you'd swap step 1 for a NIP-07 browser
extension (like Alby or nos2x) or a NIP-46 remote signer, so the app itself never
touches the raw private key at all — a meaningful security practice, not just a nicety.

Other useful entry points depending on your stack:

  • nostr-tools (JS/TS) — the most widely used client-side library
  • go-nostr (Go) — solid choice for relay-side or backend infrastructure
  • nostr-sdk (Rust) — used by several higher-performance clients and relay implementations
  • The NIPs repository on GitHub — the actual living spec, genuinely readable in an afternoon since the core protocol is small by design

11. Where this is heading

The protocol's minimalism is also its growth strategy: because NIP-01 barely changes, the
surface area for breaking compatibility stays small, while the NIP process lets the
ecosystem experiment quickly at the edges — group chat, long-form publishing, Lightning
payments, and interactive widgets have all been added this way without a single rewrite
of the base spec. That's a genuinely different development model from most protocols,
which tend to accumulate complexity in the core spec itself over time.

The honest way to think about where Nostr fits: it's not trying to replace every social
platform overnight. It's a substrate — a shared identity and event layer — that lets
independent developers build interoperable products without needing anyone's permission
or API key to do it. Whether that ends up mattering at large scale is still an open
question the ecosystem is actively answering in public, one NIP and one client at a time.


If you're building on Nostr and want to compare notes on relay architecture, NIP-29
groups, or Lightning/Cashu integration, drop a comment below — always interested in what
people are actually shipping on this stack.