nostr-tools

This skill provides comprehensive knowledge and patterns for working with nostr-tools, the most popular JavaScript/TypeScript library for Nostr protocol development.

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "nostr-tools" with this command: npx skills add purrgrammer/grimoire/purrgrammer-grimoire-nostr-tools

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

Related Skills

  • nostr - Nostr protocol fundamentals

  • svelte - Building Nostr UIs with Svelte

  • applesauce-core - Higher-level Nostr client utilities

  • applesauce-signers - Nostr signing abstractions

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

General

nostr

No summary provided by upstream source.

Repository SourceNeeds Review
General

react

No summary provided by upstream source.

Repository SourceNeeds Review
General

applesauce-signers

No summary provided by upstream source.

Repository SourceNeeds Review
General

applesauce-common

No summary provided by upstream source.

Repository SourceNeeds Review