Loading...
Loading...
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.
npx skill4agent add purrgrammer/grimoire nostr-toolsnpm install nostr-tools// 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';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 stringimport { 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']
});// 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
};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);// 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)
};// 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);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);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();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);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'
};// 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']
};// 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
}
});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
);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
);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);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// Parse nostr: URIs
const uri = 'nostr:npub1...';
const { type, data } = nip19.decode(uri.replace('nostr:', ''));// 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);
});import { nip57 } from 'nostr-tools';
// Validate zap receipt
const zapReceipt = await pool.get(relays, {
kinds: [9735],
'#e': [eventId]
});
const validatedZap = await nip57.validateZapRequest(zapReceipt);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);import { getEventHash } from 'nostr-tools/pure';
// Calculate event ID without signing
const eventId = getEventHash(unsignedEvent);import {
getSignature,
verifyEvent
} from 'nostr-tools/pure';
// Sign event data
const signature = getSignature(unsignedEvent, secretKey);
// Verify complete event
const isValid = verifyEvent(signedEvent);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);
}function subscribeToFeed(followedPubkeys, onEvent) {
return pool.subscribeMany(
relays,
[{ kinds: [1, 6], authors: followedPubkeys }],
{
onevent: onEvent,
oneose() {
console.log('Caught up with stored events');
}
}
);
}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;
}const seenEvents = new Set();
function handleEvent(event) {
if (seenEvents.has(event.id)) {
return; // Skip duplicate
}
seenEvents.add(event.id);
// Process event...
}