Chapter 2
Identity is a keypair
No accounts, no passwords: you are a secret key and its public key.
No sign-up form, no password, no username police. Let's forge you an identity in one click!
No sign-up form. Ever.
Every app you’ve used starts the same way: pick a username, choose a password, confirm your email. Behind the scenes, a company writes you into its database. Your account lives there, and it exists only as long as they say so.
Nostr skips all of that. Your identity is not a row in someone’s database: it’s a
- a secret key (
): 32 random bytes that only you know, andprivate key Private key: The 32-byte secret that proves you are you: whoever holds it can sign events as you. Never share it. - a public key (
): computed from the secret key, and safe to show the world.pubkey Public key: Your public identity: a 32-byte secp256k1 key, shown as 64 hex characters or as an npub. Safe to share.
Think of a mailbox with a slot. The public key is the address painted on the front: you put it on business cards so people can find you. The secret key is the only key that opens the box, and here it does something even better: it lets you stamp letters with a seal nobody else can forge.
No server hands these out. You make them yourself, on your own device, in a fraction of a second. Let’s do that right now.
The key forge
Forge a fresh keypair, then watch its hex bytes get re-spelled as an npub, one bech32 character at a time.
88ce••••••••••••••••••••••••••••••••••••••••••••••••••••••••3858 32 random bytes. Whoever knows these IS you.
173c1c47ef97731d55e45f8b5df6c13c5685b06acdbeb0cf8077a82eb07d8091 Derived from the secret with secp256k1 math. Safe to share.
Hex → bech32, step by step
1. Payload bytes
2. Next 5 bits
3. Look up the bech32 alphabet
- 0 q
- 1 p
- 2 z
- 3 r
- 4 y
- 5 9
- 6 x
- 7 8
- 8 g
- 9 f
- 10 2
- 11 t
- 12 v
- 13 d
- 14 w
- 15 0
- 16 s
- 17 3
- 18 j
- 19 n
- 20 5
- 21 4
- 22 k
- 23 h
- 24 c
- 25 e
- 26 6
- 27 m
- 28 u
- 29 a
- 30 7
- 31 l
4. The result grows
npub1The last 6 characters are a checksum: one typo and wallets will notice.
Ready. Press play or step forward to encode the npub.
Press the button a few times. Every keypair is brand new: there are about 2256 possible secret keys, so the chance of two people ever rolling the same one is effectively zero. No registry is needed to keep keys unique: math does it for free.
Keys forged on this page are demo keys. They are generated in your browser and never saved or sent anywhere, but treat them as throwaway: don’t use them for a real identity.
Why can’t anyone work out my secret?
The public key is calculated from the secret key using an elliptic curve called
Here’s a toy version you can actually see. Your secret is a number of hops; the public key is where you land on the clock. Try it, then try to work backwards.
The one-way clock
A toy version of the math behind secp256k1. Pick a secret number; the public spot is where you land after that many hops of 17 around a 61-position clock.
On a 61-spot clock, guessing backwards takes a blink. On the real curve there are about 1077 spots. Every computer on Earth guessing for the lifetime of the universe would not make a dent. That’s what lets you publish your public key everywhere and sleep soundly.
Under the hood: secp256k1 & x-only keys Show details
The real operation is scalar multiplication on an elliptic curve: P = k · G, where k is your
secret key (a 256-bit integer between 1 and the curve order n − 1), G is the curve’s fixed
generator point, and P is the public point. Recovering k from P is the elliptic-curve
discrete logarithm problem, which has no known efficient solution.
NIP-01 says keys and signatures follow
BIP-340 (Schnorr signatures on
secp256k1). BIP-340 uses x-only public keys: only the 32-byte x coordinate is kept, and the y
coordinate is implied to be even. That’s why a Nostr pubkey is exactly 64 lowercase hex
characters, not the 66-character “compressed” keys you may know from elsewhere.
import { generateSecretKey, getPublicKey } from "nostr-tools/pure";
const sk = generateSecretKey(); // Uint8Array(32), from a CSPRNG
const pk = getPublicKey(sk); // 64 lowercase hex chars (x-only)
console.log(pk);Not every 32-byte value is a valid key: the secret must be below n (≈ 1.158 × 1077), and a public key must be an x coordinate that is actually on the curve.
npub and nsec: same keys, friendlier spelling
Raw keys are written in hex: 64 characters of 0–9 and a–f. Computers love hex. Humans
copy-pasting it into a chat… less so. Mix up one character and you’ve silently pointed at someone
else (or no one).
So
- your public key becomes an
:npub npub: A public key encoded in bech32 so humans can copy it safely, e.g. npub1… instead of 64 hex characters.npub1… - your secret key becomes an
:nsec nsec: A private key encoded in bech32 (nsec1…). Anyone who sees it can become you.nsec1…
The prefix tells you what you’re holding (so you notice when you’re about to paste a secret),
the alphabet skips look-alike characters like 1, b, i and o, and the last six characters are
a checksum: change any character and software will refuse it instead of quietly using the wrong
key. Scroll back up to the forge and step through the encoding: the bytes are regrouped 5 bits at a
time, and each 5-bit number picks one letter from a 32-letter alphabet.
Under the hood: NIP-19 encoding Show details
- Take the 32 payload bytes (256 bits).
- Regroup them into 5-bit words: 256 / 5 = 51.2, so 52 words, the last one padded with zero bits.
- Compute a 6-word BCH checksum over the prefix (the human-readable part,
npub) and the words. - Map all 58 words through the alphabet
qpzry9x8gf2tvdw0s3jn54khce6mua7land glue it together:hrp + "1" + chars. Result: 63 characters for every npub and nsec.
Bech32 strings are for humans only. NIP-19 is explicit: they must not be used inside events or relay messages. On the wire, it’s always hex (event trimmed to the interesting field):
{
"kind": 1,
"pubkey": "7e7e9c42a91bfef19fa929e5fda1b72e0ebc1a4c1141673e2794234d86addf4e",
"content": "gm"
}import * as nip19 from "nostr-tools/nip19";
const npub = nip19.npubEncode(pk); // "npub1…"
const nsec = nip19.nsecEncode(sk); // "nsec1…" (sk is a Uint8Array)
const { type, data } = nip19.decode(npub); // type === "npub", data === pk (hex)NIP-19 also defines note (an event id), plus “shareable identifiers” that bundle extra hints as
TLV (type-length-value) records: d tag of an addressable event). Try them
all in the standalone key tool.
Two related NIPs: NIP-49 defines
ncryptsecm/44'/1237'/<account>'/0/0). NIP-06 is now marked unrecommended in the NIPs index
in favor of a single nsec, so treat it as legacy you may meet in older wallets.
Your key is your pen
Holding a secret key isn’t just about being someone: it’s about proving it. Every post you
publish is a little JSON
Try it with the key you forged above. Then change a single letter.
Signing is a stamp only you can make
A Schnorr signature proves the demo secret key approved exactly this message. Edit one letter after signing and watch the check fail.
Not signed yet.
That’s why Nostr doesn’t need a trusted server: the proof travels with the message. We’ll take a signed event apart byte by byte in the next chapter.
Under the hood: what actually gets signed Show details
Nostr never signs your raw text. The event is serialized as
[0, pubkey, created_at, kind, tags, content], hashed with id, and the id is signed with BIP-340 Schnorr, producing a 64-byte sig
(128 hex characters). Change the content and the id changes, so the old signature no longer matches.
import { finalizeEvent, verifyEvent } from "nostr-tools/pure";
const event = finalizeEvent(
{ kind: 1, created_at: Math.floor(Date.now() / 1000), tags: [], content: "gm" },
sk,
); // adds pubkey, id and sig
verifyEvent(event); // trueGuard the nsec with your life (well, almost)
Here’s the flip side of “no company owns your account”: no company can rescue it either.
- Anyone who gets your nsec is you. They can post, follow and DM as you, forever, and you can’t change “your password”, because there isn’t one.
- If you lose your nsec with no backup, the identity is gone. That’s
, and it’s one of Nostr’s honest trade-offs (more in the last chapter).key loss Key loss: Losing or leaking your private key. Nostr has no reset button, so either case means the identity is lost.
Your npub, on the other hand, is meant to be shared: on your website, in your bio, on a T-shirt. Let’s practice telling the two apart.
Who gets which key?
Four people want something. Decide: share it or refuse?
- Your friend Bob asks for your npub
“What's your npub? I want to follow you!”
- “Nostr Support” (DM) asks for your nsec
“Your account is flagged. Send your nsec within 10 minutes to verify it.”
- A podcast host asks for your npub
“Can I put your npub in the show notes so listeners can find you?”
- A shiny new web app asks for your nsec
“Paste your nsec here to claim 21,000 free sats!”
Real apps never need you to paste your nsec. Use a
Under the hood: key hygiene for developers Show details
- Generate keys with a CSPRNG (
generateSecretKey()usescrypto.getRandomValues), never withMath.random()or a hash of something guessable. - Keep secret keys as
Uint8Arrayin memory, and don’t log them. Prefer delegating to a signer:window.nostr.signEvent()(NIP-07) or abunker://connection (NIP-46). - For backups, offer
ncryptsec(NIP-49) instead of plain nsec text. Skip NIP-06 seed phrases for new code: the NIPs index marks them unrecommended in favor of a single nsec. - Accept both hex and NIP-19 input, but store and transmit hex; decode
nostr:URIs (NIP-21) with the same NIP-19 decoder.
Quick check
Takeaways
- Your Nostr identity is a secp256k1 keypair: a secret key you keep and a public key you share.
- The public key is computed from the secret key, but you can't run that math backwards.
- npub and nsec are the same bytes as the hex keys, re-spelled in bech32 with a typo-catching checksum (NIP-19).
- Signing proves an event came from your key; anyone can verify it with your public key alone.
- Never share your nsec. There is no password reset: lose it or leak it, and that identity is gone.