nostr-tools Skill
This skill provides comprehensive knowledge and patterns for working with nostr-tools, the most popular JavaScript/TypeScript library for Nostr protocol development.
When to Use This Skill
Use this skill when:
-
Building Nostr clients or applications
-
Creating and signing Nostr events
-
Connecting to Nostr relays
-
Implementing NIP features
-
Working with Nostr keys and cryptography
-
Filtering and querying events
-
Building relay pools or connections
-
Implementing NIP-44/NIP-04 encryption
Core Concepts
nostr-tools Overview
nostr-tools provides:
-
Event handling - Create, sign, verify events
-
Key management - Generate, convert, encode keys
-
Relay communication - Connect, subscribe, publish
-
NIP implementations - NIP-04, NIP-05, NIP-19, NIP-44, etc.
-
Cryptographic operations - Schnorr signatures, encryption
-
Filter building - Query events by various criteria
Installation
npm install nostr-tools
Basic Imports
// Core functionality import { SimplePool, generateSecretKey, getPublicKey, finalizeEvent, verifyEvent } from 'nostr-tools';
// NIP-specific imports import { nip04, nip05, nip19, nip44 } from 'nostr-tools';
// Relay operations import { Relay } from 'nostr-tools/relay';
Key Management
Generating Keys
import { generateSecretKey, getPublicKey } from 'nostr-tools/pure';
// Generate new secret key (Uint8Array) const secretKey = generateSecretKey();
// Derive public key const publicKey = getPublicKey(secretKey);
console.log('Secret key:', bytesToHex(secretKey)); console.log('Public key:', publicKey); // hex string
Key Encoding (NIP-19)
import { nip19 } from 'nostr-tools';
// Encode to bech32 const nsec = nip19.nsecEncode(secretKey); const npub = nip19.npubEncode(publicKey); const note = nip19.noteEncode(eventId);
console.log(nsec); // nsec1... console.log(npub); // npub1... console.log(note); // note1...
// Decode from bech32 const { type, data } = nip19.decode(npub); // type: 'npub', data: publicKey (hex)
// Encode profile reference (nprofile) const nprofile = nip19.nprofileEncode({ pubkey: publicKey, relays: ['wss://relay.example.com'] });
// Encode event reference (nevent) const nevent = nip19.neventEncode({ id: eventId, relays: ['wss://relay.example.com'], author: publicKey, kind: 1 });
// Encode address (naddr) for replaceable events const naddr = nip19.naddrEncode({ identifier: 'my-article', pubkey: publicKey, kind: 30023, relays: ['wss://relay.example.com'] });
Event Operations
Event Structure
// Unsigned event template const eventTemplate = { kind: 1, created_at: Math.floor(Date.now() / 1000), tags: [], content: 'Hello Nostr!' };
// Signed event (after finalizeEvent) const signedEvent = { id: '...', // 32-byte sha256 hash as hex pubkey: '...', // 32-byte public key as hex created_at: 1234567890, kind: 1, tags: [], content: 'Hello Nostr!', sig: '...' // 64-byte Schnorr signature as hex };
Creating and Signing Events
import { finalizeEvent, verifyEvent } from 'nostr-tools/pure';
// Create event template const eventTemplate = { kind: 1, created_at: Math.floor(Date.now() / 1000), tags: [ ['p', publicKey], // Mention ['e', eventId, '', 'reply'], // Reply ['t', 'nostr'] // Hashtag ], content: 'Hello Nostr!' };
// Sign event const signedEvent = finalizeEvent(eventTemplate, secretKey);
// Verify event const isValid = verifyEvent(signedEvent); console.log('Event valid:', isValid);
Event Kinds
// Common event kinds const KINDS = { Metadata: 0, // Profile metadata (NIP-01) Text: 1, // Short text note (NIP-01) RecommendRelay: 2, // Relay recommendation Contacts: 3, // Contact list (NIP-02) EncryptedDM: 4, // Encrypted DM (NIP-04) EventDeletion: 5, // Delete events (NIP-09) Repost: 6, // Repost (NIP-18) Reaction: 7, // Reaction (NIP-25) ChannelCreation: 40, // Channel (NIP-28) ChannelMessage: 42, // Channel message Zap: 9735, // Zap receipt (NIP-57) Report: 1984, // Report (NIP-56) RelayList: 10002, // Relay list (NIP-65) Article: 30023, // Long-form content (NIP-23) };
Creating Specific Events
// Profile metadata (kind 0) const profileEvent = finalizeEvent({ kind: 0, created_at: Math.floor(Date.now() / 1000), tags: [], content: JSON.stringify({ name: 'Alice', about: 'Nostr enthusiast', picture: 'https://example.com/avatar.jpg', nip05: 'alice@example.com', lud16: 'alice@getalby.com' }) }, secretKey);
// Contact list (kind 3) const contactsEvent = finalizeEvent({ kind: 3, created_at: Math.floor(Date.now() / 1000), tags: [ ['p', pubkey1, 'wss://relay1.com', 'alice'], ['p', pubkey2, 'wss://relay2.com', 'bob'], ['p', pubkey3, '', 'carol'] ], content: '' // Or JSON relay preferences }, secretKey);
// Reply to an event const replyEvent = finalizeEvent({ kind: 1, created_at: Math.floor(Date.now() / 1000), tags: [ ['e', rootEventId, '', 'root'], ['e', parentEventId, '', 'reply'], ['p', parentEventPubkey] ], content: 'This is a reply' }, secretKey);
// Reaction (kind 7) const reactionEvent = finalizeEvent({ kind: 7, created_at: Math.floor(Date.now() / 1000), tags: [ ['e', eventId], ['p', eventPubkey] ], content: '+' // or '-' or emoji }, secretKey);
// Delete event (kind 5) const deleteEvent = finalizeEvent({ kind: 5, created_at: Math.floor(Date.now() / 1000), tags: [ ['e', eventIdToDelete], ['e', anotherEventIdToDelete] ], content: 'Deletion reason' }, secretKey);
Relay Communication
Using SimplePool
SimplePool is the recommended way to interact with multiple relays:
import { SimplePool } from 'nostr-tools/pool';
const pool = new SimplePool(); const relays = [ 'wss://relay.damus.io', 'wss://nos.lol', 'wss://relay.nostr.band' ];
// Subscribe to events const subscription = pool.subscribeMany( relays, [ { kinds: [1], authors: [publicKey], limit: 10 } ], { onevent(event) { console.log('Received event:', event); }, oneose() { console.log('End of stored events'); } } );
// Close subscription when done subscription.close();
// Publish event to all relays const results = await Promise.allSettled( pool.publish(relays, signedEvent) );
// Query events (returns Promise) const events = await pool.querySync(relays, { kinds: [0], authors: [publicKey] });
// Get single event const event = await pool.get(relays, { ids: [eventId] });
// Close pool when done pool.close(relays);
Direct Relay Connection
import { Relay } from 'nostr-tools/relay';
const relay = await Relay.connect('wss://relay.damus.io');
console.log(Connected to ${relay.url});
// Subscribe const sub = relay.subscribe([ { kinds: [1], limit: 100 } ], { onevent(event) { console.log('Event:', event); }, oneose() { console.log('EOSE'); sub.close(); } });
// Publish await relay.publish(signedEvent);
// Close relay.close();
Handling Connection States
import { Relay } from 'nostr-tools/relay';
const relay = await Relay.connect('wss://relay.example.com');
// Listen for disconnect relay.onclose = () => { console.log('Relay disconnected'); };
// Check connection status console.log('Connected:', relay.connected);
Filters
Filter Structure
const filter = { // Event IDs ids: ['abc123...'],
// Authors (pubkeys) authors: ['pubkey1', 'pubkey2'],
// Event kinds kinds: [1, 6, 7],
// Tags (single-letter keys) '#e': ['eventId1', 'eventId2'], '#p': ['pubkey1'], '#t': ['nostr', 'bitcoin'], '#d': ['article-identifier'],
// Time range since: 1704067200, // Unix timestamp until: 1704153600,
// Limit results limit: 100,
// Search (NIP-50, if relay supports) search: 'nostr protocol' };
Common Filter Patterns
// User's recent posts const userPosts = { kinds: [1], authors: [userPubkey], limit: 50 };
// User's profile const userProfile = { kinds: [0], authors: [userPubkey] };
// User's contacts const userContacts = { kinds: [3], authors: [userPubkey] };
// Replies to an event const replies = { kinds: [1], '#e': [eventId] };
// Reactions to an event const reactions = { kinds: [7], '#e': [eventId] };
// Feed from followed users const feed = { kinds: [1, 6], authors: followedPubkeys, limit: 100 };
// Events mentioning user const mentions = { kinds: [1], '#p': [userPubkey], limit: 50 };
// Hashtag search const hashtagEvents = { kinds: [1], '#t': ['bitcoin'], limit: 100 };
// Replaceable event by d-tag const replaceableEvent = { kinds: [30023], authors: [authorPubkey], '#d': ['article-slug'] };
Multiple Filters
// Subscribe with multiple filters (OR logic) const filters = [ { kinds: [1], authors: [userPubkey], limit: 20 }, { kinds: [1], '#p': [userPubkey], limit: 20 } ];
pool.subscribeMany(relays, filters, { onevent(event) { // Receives events matching ANY filter } });
Encryption
NIP-04 (Legacy DMs)
import { nip04 } from 'nostr-tools';
// Encrypt message const ciphertext = await nip04.encrypt( secretKey, recipientPubkey, 'Hello, this is secret!' );
// Create encrypted DM event const dmEvent = finalizeEvent({ kind: 4, created_at: Math.floor(Date.now() / 1000), tags: [['p', recipientPubkey]], content: ciphertext }, secretKey);
// Decrypt message const plaintext = await nip04.decrypt( secretKey, senderPubkey, ciphertext );
NIP-44 (Modern Encryption)
import { nip44 } from 'nostr-tools';
// Get conversation key (cache this for multiple messages) const conversationKey = nip44.getConversationKey( secretKey, recipientPubkey );
// Encrypt const ciphertext = nip44.encrypt( 'Hello with NIP-44!', conversationKey );
// Decrypt const plaintext = nip44.decrypt( ciphertext, conversationKey );
NIP Implementations
NIP-05 (DNS Identifier)
import { nip05 } from 'nostr-tools';
// Query NIP-05 identifier const profile = await nip05.queryProfile('alice@example.com');
if (profile) { console.log('Pubkey:', profile.pubkey); console.log('Relays:', profile.relays); }
// Verify NIP-05 for a pubkey const isValid = await nip05.queryProfile('alice@example.com') .then(p => p?.pubkey === expectedPubkey);
NIP-10 (Reply Threading)
import { nip10 } from 'nostr-tools';
// Parse reply tags const parsed = nip10.parse(event);
console.log('Root:', parsed.root); // Original event console.log('Reply:', parsed.reply); // Direct parent console.log('Mentions:', parsed.mentions); // Other mentions console.log('Profiles:', parsed.profiles); // Mentioned pubkeys
NIP-21 (nostr: URIs)
// Parse nostr: URIs const uri = 'nostr:npub1...'; const { type, data } = nip19.decode(uri.replace('nostr:', ''));
NIP-27 (Content References)
// Parse nostr:npub and nostr:note references in content const content = 'Check out nostr:npub1abc... and nostr:note1xyz...';
const references = content.match(/nostr:(n[a-z]+1[a-z0-9]+)/g); references?.forEach(ref => { const decoded = nip19.decode(ref.replace('nostr:', '')); console.log(decoded.type, decoded.data); });
NIP-57 (Zaps)
import { nip57 } from 'nostr-tools';
// Validate zap receipt const zapReceipt = await pool.get(relays, { kinds: [9735], '#e': [eventId] });
const validatedZap = await nip57.validateZapRequest(zapReceipt);
Utilities
Hex and Bytes Conversion
import { bytesToHex, hexToBytes } from '@noble/hashes/utils';
// Convert secret key to hex const secretKeyHex = bytesToHex(secretKey);
// Convert hex back to bytes const secretKeyBytes = hexToBytes(secretKeyHex);
Event ID Calculation
import { getEventHash } from 'nostr-tools/pure';
// Calculate event ID without signing const eventId = getEventHash(unsignedEvent);
Signature Operations
import { getSignature, verifyEvent } from 'nostr-tools/pure';
// Sign event data const signature = getSignature(unsignedEvent, secretKey);
// Verify complete event const isValid = verifyEvent(signedEvent);
Best Practices
Connection Management
-
Use SimplePool - Manages connections efficiently
-
Limit concurrent connections - Don't connect to too many relays
-
Handle disconnections - Implement reconnection logic
-
Close subscriptions - Always close when done
Event Handling
-
Verify events - Always verify signatures
-
Deduplicate - Events may come from multiple relays
-
Handle replaceable events - Latest by created_at wins
-
Validate content - Don't trust event content blindly
Key Security
-
Never expose secret keys - Keep in secure storage
-
Use NIP-07 in browsers - Let extensions handle signing
-
Validate input - Check key formats before use
Performance
-
Cache events - Avoid re-fetching
-
Use filters wisely - Be specific, use limits
-
Batch operations - Combine related queries
-
Close idle connections - Free up resources
Common Patterns
Building a Feed
const pool = new SimplePool(); const relays = ['wss://relay.damus.io', 'wss://nos.lol'];
async function loadFeed(followedPubkeys) { const events = await pool.querySync(relays, { kinds: [1, 6], authors: followedPubkeys, limit: 100 });
// Sort by timestamp return events.sort((a, b) => b.created_at - a.created_at); }
Real-time Updates
function subscribeToFeed(followedPubkeys, onEvent) { return pool.subscribeMany( relays, [{ kinds: [1, 6], authors: followedPubkeys }], { onevent: onEvent, oneose() { console.log('Caught up with stored events'); } } ); }
Profile Loading
async function loadProfile(pubkey) { const [metadata] = await pool.querySync(relays, { kinds: [0], authors: [pubkey], limit: 1 });
if (metadata) { return JSON.parse(metadata.content); } return null; }
Event Deduplication
const seenEvents = new Set();
function handleEvent(event) { if (seenEvents.has(event.id)) { return; // Skip duplicate } seenEvents.add(event.id);
// Process event... }
Troubleshooting
Common Issues
Events not publishing:
-
Check relay is writable
-
Verify event is properly signed
-
Check relay's accepted kinds
Subscription not receiving events:
-
Verify filter syntax
-
Check relay has matching events
-
Ensure subscription isn't closed
Signature verification fails:
-
Check event structure is correct
-
Verify keys are in correct format
-
Ensure event hasn't been modified
NIP-05 lookup fails:
-
Check CORS headers on server
-
Verify .well-known path is correct
-
Handle network timeouts
References
-
nostr-tools GitHub: https://github.com/nbd-wtf/nostr-tools
-
Nostr Protocol: https://github.com/nostr-protocol/nostr
-
NIPs Repository: https://github.com/nostr-protocol/nips
-
NIP-01 (Basic Protocol): https://github.com/nostr-protocol/nips/blob/master/01.md
Related Skills
-
nostr - Nostr protocol fundamentals
-
svelte - Building Nostr UIs with Svelte
-
applesauce-core - Higher-level Nostr client utilities
-
applesauce-signers - Nostr signing abstractions