nostr-tools

majiayu000's avatarfrom majiayu000

This skill should be used when working with nostr-tools library for Nostr protocol operations, including event creation, signing, filtering, relay communication, and NIP implementations. Provides comprehensive knowledge of nostr-tools APIs and patterns.

⭐0stars🔀0forks📁View on GitHub🕐Updated Jan 5, 2026

When & Why to Use This Skill

This Claude skill provides comprehensive expertise in the nostr-tools library, the industry-standard JavaScript/TypeScript toolkit for the Nostr protocol. It streamlines the development of decentralized applications by offering precise guidance on event creation, cryptographic signing, relay communication, and the implementation of various Nostr Implementation Possibilities (NIPs). By leveraging this skill, developers can significantly accelerate the building of secure, interoperable, and high-performance Nostr-based software.

Use Cases

  • Building decentralized social media clients with features like global feeds, user profiles (NIP-01), and contact lists (NIP-02).
  • Implementing secure end-to-end encrypted messaging using NIP-04 (legacy) and NIP-44 (modern) standards for private communication.
  • Managing Nostr identities and cryptographic keys, including bech32 encoding/decoding (NIP-19) and DNS-based verification (NIP-05).
  • Developing automated relay bots or monitoring tools that use SimplePool to efficiently filter, query, and publish events across multiple relays.
  • Integrating advanced protocol features such as Zaps (NIP-57), long-form content (NIP-23), and event deletion (NIP-09).
namenostr-tools
descriptionThis skill should be used when working with nostr-tools library for Nostr protocol operations, including event creation, signing, filtering, relay communication, and NIP implementations. Provides comprehensive knowledge of nostr-tools APIs and patterns.

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

  1. Use SimplePool - Manages connections efficiently
  2. Limit concurrent connections - Don't connect to too many relays
  3. Handle disconnections - Implement reconnection logic
  4. Close subscriptions - Always close when done

Event Handling

  1. Verify events - Always verify signatures
  2. Deduplicate - Events may come from multiple relays
  3. Handle replaceable events - Latest by created_at wins
  4. Validate content - Don't trust event content blindly

Key Security

  1. Never expose secret keys - Keep in secure storage
  2. Use NIP-07 in browsers - Let extensions handle signing
  3. Validate input - Check key formats before use

Performance

  1. Cache events - Avoid re-fetching
  2. Use filters wisely - Be specific, use limits
  3. Batch operations - Combine related queries
  4. 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 - Nostr protocol fundamentals
  • svelte - Building Nostr UIs with Svelte
  • applesauce-core - Higher-level Nostr client utilities
  • applesauce-signers - Nostr signing abstractions