
Amayo ClintonIf 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.
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.
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.
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:
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.
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..."
}
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.
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.
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.
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.
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... }]
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 }]
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"]
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.
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:
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.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.
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:
kind: 9734 zap request event,
referencing the post being zapped, and sends it to the recipient's configured
Lightning address.kind: 9735 zap receipt
event back to the relays, cryptographically proving the payment happened and linking
it to the original post.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.
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.
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.
Decentralization isn't free, and a fair explainer has to say so plainly.
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.
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);
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 librarygo-nostr (Go) — solid choice for relay-side or backend infrastructurenostr-sdk (Rust) — used by several higher-performance clients and relay
implementationsThe 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.