Quick reference for common patterns and solutions to frequent NDK issues.
// Store pattern (recommended for React apps)
import { Store } from '@tanstack/store'
interface NDKState {
ndk: NDK | null
isConnected: boolean
signer?: NDKSigner
}
const ndkStore = new Store<NDKState>({
ndk: null,
isConnected: false
})
export const ndkActions = {
initialize: () => {
const ndk = new NDK({ explicitRelayUrls: relays })
ndkStore.setState({ ndk })
return ndk
},
getNDK: () => ndkStore.state.ndk,
setSigner: (signer: NDKSigner) => {
const ndk = ndkStore.state.ndk
if (ndk) {
ndk.signer = signer
ndkStore.setState({ signer })
}
}
}
// Initial data load + real-time updates
function useOrdersWithRealtime(orderId: string) {
const queryClient = useQueryClient()
const ndk = ndkActions.getNDK()
// Fetch initial data
const query = useQuery({
queryKey: ['orders', orderId],
queryFn: () => fetchOrders(orderId),
})
// Subscribe to updates
useEffect(() => {
if (!ndk || !orderId) return
const sub = ndk.subscribe(
{ kinds: [16], '#order': [orderId] },
{ closeOnEose: false }
)
sub.on('event', () => {
queryClient.invalidateQueries(['orders', orderId])
})
return () => sub.stop()
}, [ndk, orderId])
return query
}
// Parse event tags into structured data
function parseProductEvent(event: NDKEvent) {
const getTag = (name: string) =>
event.tags.find(t => t[0] === name)?.[1]
const getAllTags = (name: string) =>
event.tags.filter(t => t[0] === name).map(t => t[1])
return {
id: event.id,
slug: getTag('d'),
title: getTag('title'),
price: parseFloat(getTag('price') || '0'),
currency: event.tags.find(t => t[0] === 'price')?.[2] || 'USD',
images: getAllTags('image'),
shipping: getAllTags('shipping'),
description: event.content,
createdAt: event.created_at,
author: event.pubkey
}
}
// Separate NDK instances for different purposes
const mainNdk = new NDK({
explicitRelayUrls: ['wss://relay.damus.io', 'wss://nos.lol']
})
const zapNdk = new NDK({
explicitRelayUrls: ['wss://relay.damus.io'] // Zap-optimized relays
})
const blossomNdk = new NDK({
explicitRelayUrls: ['wss://blossom.server.com'] // Media server
})
await Promise.all([
mainNdk.connect(),
zapNdk.connect(),
blossomNdk.connect()
])
Symptoms: Subscription doesn't receive events, fetchEvents returns empty Set
Solutions:
const status = ndk.pool?.connectedRelays()
console.log('Connected relays:', status?.length)
if (status?.length === 0) {
await ndk.connect()
}
// ❌ Wrong
{ kinds: [16], 'order': [orderId] }
// ✅ Correct (note the # prefix for tags)
{ kinds: [16], '#order': [orderId] }
// Events might be too old/new
const now = Math.floor(Date.now() / 1000)
const filter = {
kinds: [1],
since: now - 86400, // Last 24 hours
until: now
}
// For real-time updates
ndk.subscribe(filter, { closeOnEose: false })
// For one-time historical fetch
ndk.subscribe(filter, { closeOnEose: true })
Symptoms: ndk is null/undefined
Solutions:
// In app entry point
const ndk = new NDK({ explicitRelayUrls: relays })
await ndk.connect()
const ndk = ndkActions.getNDK()
if (!ndk) throw new Error('NDK not initialized')
const ensureNDK = () => {
let ndk = ndkActions.getNDK()
if (!ndk) {
ndk = ndkActions.initialize()
}
return ndk
}
Symptoms: Event signing fails, publishing throws error
Solutions:
if (!ndk.signer) {
throw new Error('Please login first')
}
const signer = new NDKNip07Signer()
await signer.blockUntilReady() // ← Critical!
ndk.signer = signer
try {
const signer = new NDKNip07Signer()
await signer.blockUntilReady()
ndk.signer = signer
} catch (error) {
console.error('Browser extension not available')
// Fallback to other auth method
}
Symptoms: Same event received multiple times
Solutions:
const processedIds = new Set<string>()
sub.on('event', (event) => {
if (processedIds.has(event.id)) return
processedIds.add(event.id)
handleEvent(event)
})
const [events, setEvents] = useState<Map<string, NDKEvent>>(new Map())
sub.on('event', (event) => {
setEvents(prev => new Map(prev).set(event.id, event))
})
Symptoms: connect() hangs, never resolves
Solutions:
const connectWithTimeout = async (ndk: NDK, ms = 10000) => {
await Promise.race([
ndk.connect(),
new Promise((_, reject) =>
setTimeout(() => reject(new Error('Timeout')), ms)
)
])
}
// Start with reliable relays only
const reliableRelays = ['wss://relay.damus.io']
const ndk = new NDK({ explicitRelayUrls: reliableRelays })
const connectWithRetry = async (ndk: NDK, maxRetries = 3) => {
for (let i = 0; i < maxRetries; i++) {
try {
await connectWithTimeout(ndk, 10000)
return
} catch (error) {
console.log(`Retry ${i + 1}/${maxRetries}`)
if (i === maxRetries - 1) throw error
}
}
}
Symptoms: App gets slower, memory usage increases
Solutions:
useEffect(() => {
const sub = ndk.subscribe(filter, { closeOnEose: false })
// ← CRITICAL: cleanup
return () => {
sub.stop()
}
}, [dependencies])
const activeSubscriptions = new Set<NDKSubscription>()
const createSub = (filter: NDKFilter) => {
const sub = ndk.subscribe(filter, { closeOnEose: false })
activeSubscriptions.add(sub)
return sub
}
const stopAllSubs = () => {
activeSubscriptions.forEach(sub => sub.stop())
activeSubscriptions.clear()
}
Symptoms: fetchProfile() returns null/undefined
Solutions:
// Add more relay URLs
const ndk = new NDK({
explicitRelayUrls: [
'wss://relay.damus.io',
'wss://relay.nostr.band',
'wss://nos.lol'
]
})
// Ensure correct format
if (pubkey.startsWith('npub')) {
const user = ndk.getUser({ npub: pubkey })
} else if (/^[0-9a-f]{64}$/.test(pubkey)) {
const user = ndk.getUser({ hexpubkey: pubkey })
}
const profile = await user.fetchProfile()
const displayName = profile?.name || profile?.displayName || 'Anonymous'
const avatar = profile?.picture || '/default-avatar.png'
Symptoms: publish() succeeds but event not found in queries
Solutions:
await event.sign()
console.log('Event ID:', event.id) // Should be set
console.log('Signature:', event.sig) // Should exist
const relays = await event.publish()
console.log('Published to relays:', relays)
await event.publish()
// Wait a moment for relay propagation
await new Promise(resolve => setTimeout(resolve, 1000))
const found = await ndk.fetchEvents({ ids: [event.id] })
console.log('Event found:', found.size > 0)
Symptoms: Remote signer connection times out or fails
Solutions:
// Correct format: bunker://<remote-pubkey>?relay=wss://...
const isValidBunkerUrl = (url: string) => {
return url.startsWith('bunker://') && url.includes('?relay=')
}
const localSigner = new NDKPrivateKeySigner(privateKey)
await localSigner.blockUntilReady()
const remoteSigner = new NDKNip46Signer(ndk, bunkerUrl, localSigner)
await remoteSigner.blockUntilReady()
// Save for future sessions
localStorage.setItem('local-signer-key', localSigner.privateKey)
localStorage.setItem('bunker-url', bunkerUrl)
// ❌ Slow: Multiple sequential queries
const products = await ndk.fetchEvents({ kinds: [30402], authors: [pk1] })
const orders = await ndk.fetchEvents({ kinds: [16], authors: [pk1] })
const profiles = await ndk.fetchEvents({ kinds: [0], authors: [pk1] })
// ✅ Fast: Parallel queries
const [products, orders, profiles] = await Promise.all([
ndk.fetchEvents({ kinds: [30402], authors: [pk1] }),
ndk.fetchEvents({ kinds: [16], authors: [pk1] }),
ndk.fetchEvents({ kinds: [0], authors: [pk1] })
])
const profileCache = new Map<string, NDKUserProfile>()
const getCachedProfile = async (ndk: NDK, pubkey: string) => {
if (profileCache.has(pubkey)) {
return profileCache.get(pubkey)!
}
const user = ndk.getUser({ hexpubkey: pubkey })
const profile = await user.fetchProfile()
if (profile) {
profileCache.set(pubkey, profile)
}
return profile
}
// Always use limit to prevent over-fetching
const filter: NDKFilter = {
kinds: [1],
authors: [pubkey],
limit: 50 // ← Important!
}
import { debounce } from 'lodash'
const debouncedUpdate = debounce((event: NDKEvent) => {
handleEvent(event)
}, 300)
sub.on('event', debouncedUpdate)
const mockNDK = {
fetchEvents: vi.fn().mockResolvedValue(new Set()),
subscribe: vi.fn().mockReturnValue({
on: vi.fn(),
stop: vi.fn()
}),
signer: {
user: vi.fn().mockResolvedValue({ pubkey: 'test-pubkey' })
}
} as unknown as NDK
const createTestEvent = (overrides?: Partial<NDKEvent>): NDKEvent => {
return {
id: 'test-id',
kind: 1,
content: 'test content',
tags: [],
created_at: Math.floor(Date.now() / 1000),
pubkey: 'test-pubkey',
sig: 'test-sig',
...overrides
} as NDKEvent
}
For more detailed information, see:
ndk-skill.md - Complete referencequick-reference.md - Quick lookupexamples/ - Code examples