Implementing NIPs
This guide covers how to implement various NIPs in your Nostr applications.
Understanding NIP Structure
NIPs typically define:
- Event kinds to use
- Tag formats
- Content structure
- Expected behavior
Essential NIPs
NIP-01: Basic Protocol
The foundation—every client must implement this.
// Event structure
const event = {
id: '', // sha256 of serialized event
pubkey: '', // author's public key
created_at: Math.floor(Date.now() / 1000),
kind: 1,
tags: [],
content: '',
sig: '' // Schnorr signature
};
// Signing
import { finalizeEvent } from 'nostr-tools/pure';
const signedEvent = finalizeEvent(event, secretKey);
NIP-02: Follow List
Store who a user follows:
// Create/update follow list
const followList = {
kind: 3,
content: JSON.stringify({
'wss://relay.example.com': { read: true, write: true }
}),
tags: follows.map(f => ['p', f.pubkey, f.relay || '', f.petname || ''])
};
// Parse follow list
function parseFollows(event) {
return event.tags
.filter(t => t[0] === 'p')
.map(t => ({
pubkey: t[1],
relay: t[2] || null,
petname: t[3] || null
}));
}
NIP-05: DNS Verification
Verify identity via DNS:
async function verifyNip05(identifier) {
const [name, domain] = identifier.split('@');
const url = `https://${domain}/.well-known/nostr.json?name=${name}`;
try {
const response = await fetch(url);
const data = await response.json();
if (data.names && data.names[name]) {
return {
valid: true,
pubkey: data.names[name],
relays: data.relays?.[data.names[name]] || []
};
}
} catch (error) {
console.error('NIP-05 verification failed:', error);
}
return { valid: false };
}
// Server-side: /.well-known/nostr.json
{
"names": {
"alice": "pubkey-hex-here"
},
"relays": {
"pubkey-hex-here": ["wss://relay.example.com"]
}
}
NIP-10: Thread Markers
Implement proper threading:
function createReply(replyingTo, rootEvent, content) {
const tags = [];
// Root reference
if (rootEvent) {
tags.push(['e', rootEvent.id, '', 'root']);
} else {
// This is a reply to a root event
tags.push(['e', replyingTo.id, '', 'root']);
}
// Reply reference
if (rootEvent) {
tags.push(['e', replyingTo.id, '', 'reply']);
}
// Author references
const mentionedPubkeys = new Set([replyingTo.pubkey]);
if (rootEvent) {
mentionedPubkeys.add(rootEvent.pubkey);
}
for (const pk of mentionedPubkeys) {
tags.push(['p', pk]);
}
return {
kind: 1,
content,
tags
};
}
NIP-19: Bech32 Encoding
Handle human-readable identifiers:
import { nip19 } from 'nostr-tools';
// Encode
const npub = nip19.npubEncode(pubkeyHex);
const nsec = nip19.nsecEncode(secretKeyBytes);
const noteId = nip19.noteEncode(eventIdHex);
// With metadata
const nprofile = nip19.nprofileEncode({
pubkey: pubkeyHex,
relays: ['wss://relay.example.com']
});
const nevent = nip19.neventEncode({
id: eventIdHex,
relays: ['wss://relay.example.com'],
author: pubkeyHex,
kind: 1
});
// Decode
function parseNostrUri(str) {
try {
const { type, data } = nip19.decode(str);
switch (type) {
case 'npub':
return { type: 'pubkey', pubkey: data };
case 'note':
return { type: 'event', id: data };
case 'nprofile':
return { type: 'profile', ...data };
case 'nevent':
return { type: 'event', ...data };
case 'naddr':
return { type: 'address', ...data };
default:
return null;
}
} catch {
return null;
}
}
NIP-25: Reactions
Like, dislike, or emoji reactions:
function createReaction(targetEvent, reaction = '+') {
return {
kind: 7,
content: reaction, // '+', '-', or emoji
tags: [
['e', targetEvent.id],
['p', targetEvent.pubkey]
]
};
}
// Get reactions for an event
async function getReactions(eventId) {
const events = await pool.querySync(relays, {
kinds: [7],
'#e': [eventId]
});
const counts = { '+': 0, '-': 0 };
const emojis = {};
for (const event of events) {
if (event.content === '+') counts['+']++;
else if (event.content === '-') counts['-']++;
else {
emojis[event.content] = (emojis[event.content] || 0) + 1;
}
}
return { counts, emojis };
}
NIP-57: Zaps (Lightning)
Implement Lightning payments:
// 1. Get zap endpoint from user's profile
async function getZapEndpoint(pubkey) {
const profile = await getProfile(pubkey);
const lud16 = profile?.lud16;
if (!lud16) return null;
const [name, domain] = lud16.split('@');
const url = `https://${domain}/.well-known/lnurlp/${name}`;
const response = await fetch(url);
return response.json();
}
// 2. Create zap request
function createZapRequest(recipientPubkey, eventId, amount, relays) {
return {
kind: 9734,
content: '',
tags: [
['p', recipientPubkey],
['amount', String(amount * 1000)], // millisats
['relays', ...relays],
...(eventId ? [['e', eventId]] : [])
]
};
}
// 3. Get invoice from LNURL endpoint
async function getZapInvoice(zapEndpoint, zapRequest, amount) {
const signedRequest = await signEvent(zapRequest);
const encoded = encodeURIComponent(JSON.stringify(signedRequest));
const url = `${zapEndpoint.callback}?amount=${amount * 1000}&nostr=${encoded}`;
const response = await fetch(url);
const data = await response.json();
return data.pr; // Lightning invoice
}
Advanced NIPs
NIP-42: Relay Authentication
Handle authentication challenges:
function handleAuthChallenge(challenge, relayUrl) {
const authEvent = {
kind: 22242,
content: '',
tags: [
['relay', relayUrl],
['challenge', challenge]
],
created_at: Math.floor(Date.now() / 1000)
};
return signEvent(authEvent);
}
// In WebSocket handler
ws.onmessage = async (msg) => {
const [type, ...data] = JSON.parse(msg.data);
if (type === 'AUTH') {
const challenge = data[0];
const authEvent = await handleAuthChallenge(challenge, ws.url);
ws.send(JSON.stringify(['AUTH', authEvent]));
}
};
NIP-44: Encryption
Modern encryption for DMs:
import { nip44 } from 'nostr-tools';
// Encrypt
function encryptMessage(senderSk, recipientPk, plaintext) {
const conversationKey = nip44.v2.utils.getConversationKey(
senderSk,
recipientPk
);
return nip44.v2.encrypt(plaintext, conversationKey);
}
// Decrypt
function decryptMessage(recipientSk, senderPk, ciphertext) {
const conversationKey = nip44.v2.utils.getConversationKey(
recipientSk,
senderPk
);
return nip44.v2.decrypt(ciphertext, conversationKey);
}
NIP-65: Relay List Metadata
User's relay preferences:
// Publish relay list
function createRelayListEvent(relays) {
return {
kind: 10002,
content: '',
tags: relays.map(r => {
if (r.read && r.write) return ['r', r.url];
if (r.read) return ['r', r.url, 'read'];
if (r.write) return ['r', r.url, 'write'];
}).filter(Boolean)
};
}
// Parse relay list
function parseRelayList(event) {
return event.tags
.filter(t => t[0] === 'r')
.map(t => ({
url: t[1],
read: !t[2] || t[2] === 'read',
write: !t[2] || t[2] === 'write'
}));
}
Testing NIP Compliance
// Verify event structure
function validateEvent(event) {
const errors = [];
if (!event.id || event.id.length !== 64) {
errors.push('Invalid event ID');
}
if (!event.pubkey || event.pubkey.length !== 64) {
errors.push('Invalid pubkey');
}
if (typeof event.created_at !== 'number') {
errors.push('Invalid created_at');
}
if (typeof event.kind !== 'number') {
errors.push('Invalid kind');
}
if (!Array.isArray(event.tags)) {
errors.push('Invalid tags');
}
if (typeof event.content !== 'string') {
errors.push('Invalid content');
}
if (!event.sig || event.sig.length !== 128) {
errors.push('Invalid signature');
}
return errors;
}