This skill should be used when working with the Nostr protocol, implementing Nostr clients or relays, handling Nostr events, or discussing Nostr Implementation Possibilities (NIPs)...
Comprehensive reference for the Nostr protocol and the nostr-tools JavaScript library.
All data in Nostr is an event — a signed JSON object:
{
"id": "<32-byte hex SHA256 of serialized event>",
"pubkey": "<32-byte hex public key>",
"created_at": "<unix timestamp in seconds>",
"kind": "<integer>",
"tags": [["<name>", "<value>", "..."]],
"content": "<string>",
"sig": "<64-byte hex schnorr signature of id>"
}
ID calculation: SHA256(JSON.stringify([0, pubkey, created_at, kind, tags, content])) — compact JSON, no spaces.
Signature: Schnorr (BIP-340) on secp256k1, signing the 32-byte event ID.
| Range | Type | Behavior |
|---|---|---|
| 0-999 | Core | Varies per kind |
| 1000-9999 | Regular | Immutable, all kept |
| 10000-19999 | Replaceable | Latest per pubkey+kind wins |
| 20000-29999 | Ephemeral | Not stored, forwarded only |
| 30000-39999 | Parameterized Replaceable | Latest per pubkey+kind+d-tag wins |
Common kinds: 0 metadata, 1 text note, 3 contacts, 5 deletion, 6 repost, 7 reaction, 9 group message (NIP-29), 9735 zap receipt, 10002 relay list, 30023 article.
See references/event-kinds.md for the full list.
["e", "<event-id>", "<relay-url>", "<marker>"] — event reference
["p", "<pubkey>", "<relay-url>"] — profile reference
["a", "<kind>:<pubkey>:<d-tag>", "<relay-url>"] — replaceable event ref
["d", "<identifier>"] — parameterized replaceable ID
["t", "<hashtag>"] — hashtag
["r", "<url>"] — web resource / relay
NIP-10 markers for threading: "root", "reply", "mention".
Client sends: ["EVENT", <event>], ["REQ", <sub_id>, <filter>, ...], ["CLOSE", <sub_id>], ["AUTH", <event>]
Relay sends: ["EVENT", <sub_id>, <event>], ["OK", <event_id>, <bool>, <msg>], ["EOSE", <sub_id>], ["CLOSED", <sub_id>, <msg>], ["NOTICE", <msg>], ["AUTH", <challenge>]
{
"ids": ["<hex>"],
"authors": ["<hex>"],
"kinds": [1, 7],
"#e": ["<event-id>"],
"#p": ["<pubkey>"],
"#t": ["<hashtag>"],
"since": 1704067200,
"until": 1704153600,
"limit": 50,
"search": "query"
}
ids and authors support prefix matchinglimit (50-500)import { generateSecretKey, getPublicKey } from 'nostr-tools/pure';
import { nip19 } from 'nostr-tools';
const sk = generateSecretKey(); // Uint8Array
const pk = getPublicKey(sk); // hex string
// Encode
const npub = nip19.npubEncode(pk); // npub1...
const nsec = nip19.nsecEncode(sk); // nsec1...
const note = nip19.noteEncode(eventId); // note1...
const nprofile = nip19.nprofileEncode({ pubkey: pk, relays: ['wss://...'] });
const nevent = nip19.neventEncode({ id, relays, author, kind });
const naddr = nip19.naddrEncode({ identifier, pubkey, kind, relays });
// Decode
const { type, data } = nip19.decode(npub); // type: 'npub', data: hex
import { finalizeEvent, verifyEvent } from 'nostr-tools/pure';
const event = finalizeEvent({
kind: 1,
created_at: Math.floor(Date.now() / 1000),
tags: [['t', 'nostr']],
content: 'Hello Nostr!'
}, secretKey);
const valid = verifyEvent(event); // true/false
import { SimplePool } from 'nostr-tools/pool';
const pool = new SimplePool();
const relays = ['wss://relay.damus.io', 'wss://nos.lol'];
// Subscribe
const sub = pool.subscribeMany(relays, [filter], {
onevent(event) { /* handle event */ },
oneose() { /* end of stored events */ }
});
sub.close(); // always close when done
// Query (returns Promise)
const events = await pool.querySync(relays, filter);
const event = await pool.get(relays, { ids: [eventId] });
// Publish
await Promise.allSettled(pool.publish(relays, signedEvent));
// Cleanup
pool.close(relays);
import { Relay } from 'nostr-tools/relay';
const relay = await Relay.connect('wss://relay.damus.io');
const sub = relay.subscribe([filter], {
onevent(event) { /* ... */ },
oneose() { sub.close(); }
});
await relay.publish(signedEvent);
relay.close();
// NIP-44 (preferred)
import { nip44 } from 'nostr-tools';
const conversationKey = nip44.getConversationKey(secretKey, recipientPubkey);
const ciphertext = nip44.encrypt('Hello!', conversationKey);
const plaintext = nip44.decrypt(ciphertext, conversationKey);
// NIP-04 (legacy, avoid for new code)
import { nip04 } from 'nostr-tools';
const ct = await nip04.encrypt(secretKey, recipientPubkey, 'Hello!');
const pt = await nip04.decrypt(secretKey, senderPubkey, ct);
import { nip05 } from 'nostr-tools';
const profile = await nip05.queryProfile('alice@example.com');
// { pubkey: '...', relays: ['wss://...'] }
import { nip10 } from 'nostr-tools';
const parsed = nip10.parse(event);
// { root, reply, mentions, profiles }
| NIP | Topic | Key Points |
|---|---|---|
| 01 | Basic protocol | Event structure, WebSocket messages, filters |
| 02 | Contacts | Kind 3, p tags for following list |
| 05 | DNS identifiers | name@domain via .well-known/nostr.json |
| 09 | Deletion | Kind 5, e tags for events to delete |
| 10 | Threading | e tag markers: root, reply, mention |
| 11 | Relay info | HTTP GET relay URL for NIP-11 document |
| 19 | bech32 entities | npub, nsec, note, nprofile, nevent, naddr |
| 25 | Reactions | Kind 7, content "+" or emoji |
| 42 | Auth | Relay challenges, kind 22242 response |
| 44 | Encryption | ChaCha20-Poly1305 AEAD (replaces NIP-04) |
| 50 | Search | search field in filters |
| 57 | Zaps | Kind 9734 request, 9735 receipt |
| 65 | Relay list | Kind 10002, read/write relay preferences |
See references/nips-overview.md for comprehensive NIP documentation.
const reply = finalizeEvent({
kind: 1,
created_at: Math.floor(Date.now() / 1000),
tags: [
['e', rootEventId, '', 'root'],
['e', parentEventId, '', 'reply'],
['p', parentAuthorPubkey]
],
content: 'Great post!'
}, secretKey);
// Fetch user's relay preferences
const [relayList] = await pool.querySync(relays, {
kinds: [10002], authors: [pubkey]
});
const readRelays = [], writeRelays = [];
for (const [tag, url, mode] of relayList.tags) {
if (tag !== 'r') continue;
if (!mode || mode === 'read') readRelays.push(url);
if (!mode || mode === 'write') writeRelays.push(url);
}
let delay = 1000;
const connect = () => {
const ws = new WebSocket(relayUrl);
ws.onopen = () => { delay = 1000; resubscribe(); };
ws.onclose = () => {
setTimeout(connect, delay);
delay = Math.min(delay * 2, 30000);
};
};
See references/common-mistakes.md for a comprehensive list of pitfalls.