- Introduced comprehensive documentation for the Nostr Development Kit (NDK) including an overview, quick reference, and troubleshooting guide. - Added detailed examples covering initialization, authentication, event publishing, querying, and user profile management. - Structured the documentation to facilitate quick lookups and deep learning, based on real-world usage patterns from the Plebeian Market application. - Created an index for examples to enhance usability and navigation. - Bumped version to 1.0.0 to reflect the addition of this new skill set.
17 KiB
17 KiB
NDK (Nostr Development Kit) - Claude Skill Reference
Overview
NDK is the primary Nostr development kit with outbox-model support, designed for building Nostr applications with TypeScript/JavaScript. This reference is based on analyzing production usage in the Plebeian Market codebase.
Core Concepts
1. NDK Initialization
Basic Pattern:
import NDK from '@nostr-dev-kit/ndk'
// Simple initialization
const ndk = new NDK({
explicitRelayUrls: ['wss://relay.damus.io', 'wss://relay.nostr.band']
})
await ndk.connect()
Store-based Pattern (Production):
// From src/lib/stores/ndk.ts
const ndk = new NDK({
explicitRelayUrls: relays || defaultRelaysUrls,
})
// Separate NDK for zaps on specialized relays
const zapNdk = new NDK({
explicitRelayUrls: ZAP_RELAYS,
})
// Connect with timeout protection
const connectPromise = ndk.connect()
const timeoutPromise = new Promise((_, reject) =>
setTimeout(() => reject(new Error('Connection timeout')), timeoutMs)
)
await Promise.race([connectPromise, timeoutPromise])
2. Authentication & Signers
NDK supports multiple signer types for different authentication methods:
NIP-07 (Browser Extension)
import { NDKNip07Signer } from '@nostr-dev-kit/ndk'
const signer = new NDKNip07Signer()
await signer.blockUntilReady()
ndk.signer = signer
const user = await signer.user()
Private Key Signer
import { NDKPrivateKeySigner } from '@nostr-dev-kit/ndk'
const signer = new NDKPrivateKeySigner(privateKeyHex)
await signer.blockUntilReady()
ndk.signer = signer
const user = await signer.user()
NIP-46 (Remote Signer / Bunker)
import { NDKNip46Signer } from '@nostr-dev-kit/ndk'
const localSigner = new NDKPrivateKeySigner(localPrivateKey)
const remoteSigner = new NDKNip46Signer(ndk, bunkerUrl, localSigner)
await remoteSigner.blockUntilReady()
ndk.signer = remoteSigner
const user = await remoteSigner.user()
Key Points:
- Always call
blockUntilReady()before using a signer - Store signer reference in your state management
- Set
ndk.signerto enable signing operations - Use
await signer.user()to get the authenticated user
3. Event Creation & Publishing
Basic Event Pattern
import { NDKEvent } from '@nostr-dev-kit/ndk'
// Create event
const event = new NDKEvent(ndk)
event.kind = 1 // Kind 1 = text note
event.content = "Hello Nostr!"
event.tags = [
['t', 'nostr'],
['p', recipientPubkey]
]
// Sign and publish
await event.sign() // Uses ndk.signer automatically
await event.publish()
// Get event ID after signing
console.log(event.id)
Production Pattern with Error Handling
// From src/publish/orders.tsx
const event = new NDKEvent(ndk)
event.kind = ORDER_PROCESS_KIND
event.content = orderNotes || ''
event.tags = [
['p', sellerPubkey],
['subject', `Order for ${productName}`],
['type', 'order-creation'],
['order', orderId],
['amount', totalAmount],
['item', productRef, quantity.toString()],
]
// Optional tags
if (shippingRef) {
event.tags.push(['shipping', shippingRef])
}
try {
await event.sign(signer) // Can pass explicit signer
await event.publish()
return event.id
} catch (error) {
console.error('Failed to publish event:', error)
throw error
}
Key Points:
- Create event with
new NDKEvent(ndk) - Set
kind,content, andtagsproperties - Optional: Set
created_attimestamp (defaults to now) - Call
await event.sign()before publishing - Call
await event.publish()to broadcast to relays - Access
event.idafter signing for the event hash
4. Querying Events with Filters
fetchEvents() - One-time Fetch
import { NDKFilter } from '@nostr-dev-kit/ndk'
// Simple filter
const filter: NDKFilter = {
kinds: [30402], // Product listings
authors: [merchantPubkey],
limit: 50
}
const events = await ndk.fetchEvents(filter)
// Returns Set<NDKEvent>
// Convert to array and process
const eventArray = Array.from(events)
const sortedEvents = eventArray.sort((a, b) =>
(b.created_at || 0) - (a.created_at || 0)
)
Advanced Filters
// Multiple kinds
const filter: NDKFilter = {
kinds: [16, 17], // Orders and payment receipts
'#order': [orderId], // Tag filter (# prefix)
since: Math.floor(Date.now() / 1000) - 86400, // Last 24 hours
limit: 100
}
// Event ID lookup
const filter: NDKFilter = {
ids: [eventIdHex],
}
// Tag filtering
const filter: NDKFilter = {
kinds: [1],
'#p': [pubkey], // Events mentioning pubkey
'#t': ['nostr'], // Events with hashtag 'nostr'
}
5. Subscriptions (Real-time)
Basic Subscription
// From src/queries/blacklist.tsx
const filter = {
kinds: [10000],
authors: [appPubkey],
}
const subscription = ndk.subscribe(filter, {
closeOnEose: false, // Keep open for real-time updates
})
subscription.on('event', (event: NDKEvent) => {
console.log('New event received:', event)
// Process event
})
subscription.on('eose', () => {
console.log('End of stored events')
})
// Cleanup
subscription.stop()
Production Pattern with React Query
// From src/queries/orders.tsx
useEffect(() => {
if (!orderId || !ndk) return
const filter = {
kinds: [ORDER_PROCESS_KIND, PAYMENT_RECEIPT_KIND],
'#order': [orderId],
}
const subscription = ndk.subscribe(filter, {
closeOnEose: false,
})
subscription.on('event', (newEvent) => {
// Invalidate React Query cache to trigger refetch
queryClient.invalidateQueries({
queryKey: orderKeys.details(orderId)
})
})
// Cleanup on unmount
return () => {
subscription.stop()
}
}, [orderId, ndk, queryClient])
Monitoring Specific Events
// From src/queries/payment.tsx - Payment receipt monitoring
const receiptFilter = {
kinds: [17], // Payment receipts
'#order': [orderId],
'#payment-request': [invoiceId],
since: sessionStartTime - 30, // Clock skew buffer
}
const subscription = ndk.subscribe(receiptFilter, {
closeOnEose: false,
})
subscription.on('event', (receiptEvent: NDKEvent) => {
// Verify this is the correct invoice
const paymentRequestTag = receiptEvent.tags.find(
tag => tag[0] === 'payment-request'
)
if (paymentRequestTag?.[1] === invoiceId) {
const paymentTag = receiptEvent.tags.find(tag => tag[0] === 'payment')
const preimage = paymentTag?.[3] || 'external-payment'
// Stop subscription after finding payment
subscription.stop()
handlePaymentReceived(preimage)
}
})
Key Subscription Patterns:
- Use
closeOnEose: falsefor real-time monitoring - Use
closeOnEose: truefor one-time historical fetch - Always call
subscription.stop()in cleanup - Listen to both
'event'and'eose'events - Filter events in the handler for specific conditions
- Integrate with React Query for reactive UI updates
6. User & Profile Handling
Fetching User Profiles
// From src/queries/profiles.tsx
// By npub
const user = ndk.getUser({ npub })
const profile = await user.fetchProfile()
// Returns NDKUserProfile with name, picture, about, etc.
// By hex pubkey
const user = ndk.getUser({ hexpubkey: pubkey })
const profile = await user.fetchProfile()
// By NIP-05 identifier
const user = await ndk.getUserFromNip05('user@domain.com')
if (user) {
const profile = await user.fetchProfile()
}
// Profile fields
const name = profile?.name || profile?.displayName
const avatar = profile?.picture || profile?.image
const bio = profile?.about
const nip05 = profile?.nip05
const lud16 = profile?.lud16 // Lightning address
Getting Current User
// Active user (authenticated)
const user = ndk.activeUser
// From signer
const user = await ndk.signer?.user()
// User properties
const pubkey = user.pubkey // Hex format
const npub = user.npub // NIP-19 encoded
7. NDK Event Object
Essential Properties
interface NDKEvent {
id: string // Event hash (after signing)
kind: number // Event kind
content: string // Event content
tags: NDKTag[] // Array of tag arrays
created_at?: number // Unix timestamp
pubkey?: string // Author pubkey (after signing)
sig?: string // Signature (after signing)
// Methods
sign(signer?: NDKSigner): Promise<void>
publish(): Promise<void>
tagValue(tagName: string): string | undefined
}
type NDKTag = string[] // e.g., ['p', pubkey, relay, petname]
Tag Helpers
// Get first value of a tag
const orderId = event.tagValue('order')
const recipientPubkey = event.tagValue('p')
// Find specific tag
const paymentTag = event.tags.find(tag => tag[0] === 'payment')
const preimage = paymentTag?.[3]
// Get all tags of a type
const pTags = event.tags.filter(tag => tag[0] === 'p')
const allPubkeys = pTags.map(tag => tag[1])
// Common tag patterns
event.tags.push(['p', pubkey]) // Mention
event.tags.push(['e', eventId]) // Reference event
event.tags.push(['t', 'nostr']) // Hashtag
event.tags.push(['d', identifier]) // Replaceable event ID
event.tags.push(['a', '30402:pubkey:d-tag']) // Addressable event reference
8. Parameterized Replaceable Events (NIP-33)
Used for products, collections, profiles that need updates:
// Product listing (kind 30402)
const event = new NDKEvent(ndk)
event.kind = 30402
event.content = JSON.stringify(productDetails)
event.tags = [
['d', productSlug], // Unique identifier
['title', productName],
['price', price, currency],
['image', imageUrl],
['shipping', shippingRef],
]
await event.sign()
await event.publish()
// Querying replaceable events
const filter = {
kinds: [30402],
authors: [merchantPubkey],
'#d': [productSlug], // Specific product
}
const events = await ndk.fetchEvents(filter)
// Returns only the latest version due to replaceable nature
9. Relay Management
Getting Relay Status
// From src/lib/stores/ndk.ts
const connectedRelays = Array.from(ndk.pool?.relays.values() || [])
.filter(relay => relay.status === 1) // 1 = connected
.map(relay => relay.url)
const outboxRelays = Array.from(ndk.outboxPool?.relays.values() || [])
Adding Relays
// Add explicit relays
ndk.addExplicitRelay('wss://relay.example.com')
// Multiple relays
const relays = ['wss://relay1.com', 'wss://relay2.com']
relays.forEach(url => ndk.addExplicitRelay(url))
10. Common Patterns & Best Practices
Null Safety
// Always check NDK initialization
const ndk = ndkActions.getNDK()
if (!ndk) throw new Error('NDK not initialized')
// Check signer before operations requiring auth
const signer = ndk.signer
if (!signer) throw new Error('No active signer')
// Check user authentication
const user = ndk.activeUser
if (!user) throw new Error('Not authenticated')
Error Handling
try {
const events = await ndk.fetchEvents(filter)
if (events.size === 0) {
return null // No results found
}
return Array.from(events)
} catch (error) {
console.error('Failed to fetch events:', error)
throw new Error('Could not fetch data from relays')
}
Connection Lifecycle
// Initialize once at app startup
const ndk = new NDK({ explicitRelayUrls: relays })
// Connect with timeout
await Promise.race([
ndk.connect(),
new Promise((_, reject) =>
setTimeout(() => reject(new Error('Timeout')), 10000)
)
])
// Check connection status
const isConnected = ndk.pool?.connectedRelays().length > 0
// Reconnect if needed
if (!isConnected) {
await ndk.connect()
}
Subscription Cleanup
// In React components
useEffect(() => {
if (!ndk) return
const sub = ndk.subscribe(filter, { closeOnEose: false })
sub.on('event', handleEvent)
sub.on('eose', handleEose)
// Critical: cleanup on unmount
return () => {
sub.stop()
}
}, [dependencies])
Event Validation
// Check required fields before processing
if (!event.pubkey) {
console.error('Event missing pubkey')
return
}
if (!event.created_at) {
console.error('Event missing timestamp')
return
}
// Verify event age
const now = Math.floor(Date.now() / 1000)
const eventAge = now - (event.created_at || 0)
if (eventAge > 86400) { // Older than 24 hours
console.log('Event is old, skipping')
return
}
// Validate specific tags exist
const orderId = event.tagValue('order')
if (!orderId) {
console.error('Order event missing order ID')
return
}
11. Common Event Kinds
// NIP-01: Basic Events
const KIND_METADATA = 0 // User profile
const KIND_TEXT_NOTE = 1 // Short text note
const KIND_RECOMMEND_RELAY = 2 // Relay recommendation
// NIP-04: Encrypted Direct Messages
const KIND_ENCRYPTED_DM = 4
// NIP-25: Reactions
const KIND_REACTION = 7
// NIP-51: Lists
const KIND_MUTE_LIST = 10000
const KIND_PIN_LIST = 10001
const KIND_RELAY_LIST = 10002
// NIP-57: Lightning Zaps
const KIND_ZAP_REQUEST = 9734
const KIND_ZAP_RECEIPT = 9735
// Marketplace (Plebeian/Gamma spec)
const ORDER_PROCESS_KIND = 16 // Order processing
const PAYMENT_RECEIPT_KIND = 17 // Payment receipts
const DIRECT_MESSAGE_KIND = 14 // Direct messages
const ORDER_GENERAL_KIND = 27 // General order events
const SHIPPING_KIND = 30405 // Shipping options
const PRODUCT_KIND = 30402 // Product listings
const COLLECTION_KIND = 30401 // Product collections
const REVIEW_KIND = 30407 // Product reviews
// Application Handlers
const APP_HANDLER_KIND = 31990 // NIP-89 app handlers
Integration with TanStack Query
NDK works excellently with TanStack Query for reactive data fetching:
Query Functions
// From src/queries/products.tsx
export const fetchProductsByPubkey = async (pubkey: string) => {
const ndk = ndkActions.getNDK()
if (!ndk) throw new Error('NDK not initialized')
const filter: NDKFilter = {
kinds: [30402],
authors: [pubkey],
}
const events = await ndk.fetchEvents(filter)
return Array.from(events).map(parseProductEvent)
}
export const useProductsByPubkey = (pubkey: string) => {
return useQuery({
queryKey: productKeys.byAuthor(pubkey),
queryFn: () => fetchProductsByPubkey(pubkey),
enabled: !!pubkey,
staleTime: 30000,
})
}
Combining Queries with Subscriptions
// Query for initial data
const { data: order, refetch } = useQuery({
queryKey: orderKeys.details(orderId),
queryFn: () => fetchOrderById(orderId),
enabled: !!orderId,
})
// Subscription for real-time updates
useEffect(() => {
if (!orderId || !ndk) return
const sub = ndk.subscribe(
{ kinds: [16, 17], '#order': [orderId] },
{ closeOnEose: false }
)
sub.on('event', () => {
// Invalidate query to trigger refetch
queryClient.invalidateQueries({
queryKey: orderKeys.details(orderId)
})
})
return () => sub.stop()
}, [orderId, ndk, queryClient])
Troubleshooting
Events Not Received
- Check relay connections:
ndk.pool?.connectedRelays() - Verify filter syntax (especially tag filters with
#prefix) - Check event timestamps match filter's
since/until - Ensure
closeOnEose: falsefor real-time subscriptions
Signing Errors
- Verify signer is initialized:
await signer.blockUntilReady() - Check signer is set:
ndk.signer !== undefined - For NIP-07, ensure browser extension is installed and enabled
- For NIP-46, verify bunker URL and local signer are correct
Connection Timeouts
- Implement connection timeout pattern shown above
- Try connecting to fewer, more reliable relays initially
- Use fallback relays in production
Duplicate Events
- NDK deduplicates by event ID automatically
- For subscriptions, track processed event IDs if needed
- Use replaceable events (kinds 10000-19999, 30000-39999) when appropriate
Performance Optimization
Batching Queries
// Instead of multiple fetchEvents calls
const [products, orders, profiles] = await Promise.all([
ndk.fetchEvents(productFilter),
ndk.fetchEvents(orderFilter),
ndk.fetchEvents(profileFilter),
])
Limiting Results
const filter = {
kinds: [1],
authors: [pubkey],
limit: 50, // Limit results
since: recentTimestamp, // Only recent events
}
Caching with React Query
export const useProfile = (npub: string) => {
return useQuery({
queryKey: profileKeys.byNpub(npub),
queryFn: () => fetchProfileByNpub(npub),
staleTime: 5 * 60 * 1000, // 5 minutes
cacheTime: 30 * 60 * 1000, // 30 minutes
enabled: !!npub,
})
}
References
- NDK GitHub: https://github.com/nostr-dev-kit/ndk
- NDK Documentation: https://ndk.fyi
- Nostr NIPs: https://github.com/nostr-protocol/nips
- Production Example: Plebeian Market codebase
Key Files in This Codebase
src/lib/stores/ndk.ts- NDK store and initializationsrc/lib/stores/auth.ts- Authentication with NDK signerssrc/queries/*.tsx- Query patterns with NDKsrc/publish/*.tsx- Event publishing patternsscripts/gen_*.ts- Event creation examples
This reference is based on NDK version used in production and real-world patterns from the Plebeian Market application.