Files
next.orly.dev/.claude/skills/ndk/troubleshooting.md
mleku 27f92336ae Add NDK skill documentation and examples
- 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.
2025-11-06 14:34:06 +00:00

11 KiB

NDK Common Patterns & Troubleshooting

Quick reference for common patterns and solutions to frequent NDK issues.

Common Patterns

Store-Based NDK Management

// 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 })
    }
  }
}

Query + Subscription Pattern

// 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
}

Event Parsing Pattern

// 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
  }
}

Relay Pool Pattern

// 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()
])

Troubleshooting

Problem: Events Not Received

Symptoms: Subscription doesn't receive events, fetchEvents returns empty Set

Solutions:

  1. Check relay connection:
const status = ndk.pool?.connectedRelays()
console.log('Connected relays:', status?.length)
if (status?.length === 0) {
  await ndk.connect()
}
  1. Verify filter syntax (especially tags):
// ❌ Wrong
{ kinds: [16], 'order': [orderId] }

// ✅ Correct (note the # prefix for tags)
{ kinds: [16], '#order': [orderId] }
  1. Check timestamps:
// 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
}
  1. Ensure closeOnEose is correct:
// For real-time updates
ndk.subscribe(filter, { closeOnEose: false })

// For one-time historical fetch
ndk.subscribe(filter, { closeOnEose: true })

Problem: "NDK not initialized"

Symptoms: ndk is null/undefined

Solutions:

  1. Initialize before use:
// In app entry point
const ndk = new NDK({ explicitRelayUrls: relays })
await ndk.connect()
  1. Add null checks:
const ndk = ndkActions.getNDK()
if (!ndk) throw new Error('NDK not initialized')
  1. Use initialization guard:
const ensureNDK = () => {
  let ndk = ndkActions.getNDK()
  if (!ndk) {
    ndk = ndkActions.initialize()
  }
  return ndk
}

Problem: "No active signer" / Cannot Sign Events

Symptoms: Event signing fails, publishing throws error

Solutions:

  1. Check signer is set:
if (!ndk.signer) {
  throw new Error('Please login first')
}
  1. Ensure blockUntilReady called:
const signer = new NDKNip07Signer()
await signer.blockUntilReady()  // ← Critical!
ndk.signer = signer
  1. Handle NIP-07 unavailable:
try {
  const signer = new NDKNip07Signer()
  await signer.blockUntilReady()
  ndk.signer = signer
} catch (error) {
  console.error('Browser extension not available')
  // Fallback to other auth method
}

Problem: Duplicate Events in Subscriptions

Symptoms: Same event received multiple times

Solutions:

  1. Track processed event IDs:
const processedIds = new Set<string>()

sub.on('event', (event) => {
  if (processedIds.has(event.id)) return
  processedIds.add(event.id)
  handleEvent(event)
})
  1. Use Map for event storage:
const [events, setEvents] = useState<Map<string, NDKEvent>>(new Map())

sub.on('event', (event) => {
  setEvents(prev => new Map(prev).set(event.id, event))
})

Problem: Connection Timeout

Symptoms: connect() hangs, never resolves

Solutions:

  1. Use timeout wrapper:
const connectWithTimeout = async (ndk: NDK, ms = 10000) => {
  await Promise.race([
    ndk.connect(),
    new Promise((_, reject) =>
      setTimeout(() => reject(new Error('Timeout')), ms)
    )
  ])
}
  1. Try fewer relays:
// Start with reliable relays only
const reliableRelays = ['wss://relay.damus.io']
const ndk = new NDK({ explicitRelayUrls: reliableRelays })
  1. Add connection retry:
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
    }
  }
}

Problem: Subscription Memory Leak

Symptoms: App gets slower, memory usage increases

Solutions:

  1. Always stop subscriptions:
useEffect(() => {
  const sub = ndk.subscribe(filter, { closeOnEose: false })
  
  // ← CRITICAL: cleanup
  return () => {
    sub.stop()
  }
}, [dependencies])
  1. Track active subscriptions:
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()
}

Problem: Profile Not Found

Symptoms: fetchProfile() returns null/undefined

Solutions:

  1. Check different relays:
// Add more relay URLs
const ndk = new NDK({
  explicitRelayUrls: [
    'wss://relay.damus.io',
    'wss://relay.nostr.band',
    'wss://nos.lol'
  ]
})
  1. Verify pubkey format:
// 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 })
}
  1. Handle missing profiles gracefully:
const profile = await user.fetchProfile()
const displayName = profile?.name || profile?.displayName || 'Anonymous'
const avatar = profile?.picture || '/default-avatar.png'

Problem: Events Published But Not Visible

Symptoms: publish() succeeds but event not found in queries

Solutions:

  1. Verify event was signed:
await event.sign()
console.log('Event ID:', event.id)  // Should be set
console.log('Signature:', event.sig)  // Should exist
  1. Check relay acceptance:
const relays = await event.publish()
console.log('Published to relays:', relays)
  1. Query immediately after publish:
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)

Problem: NIP-46 Connection Fails

Symptoms: Remote signer connection times out or fails

Solutions:

  1. Verify bunker URL format:
// Correct format: bunker://<remote-pubkey>?relay=wss://...
const isValidBunkerUrl = (url: string) => {
  return url.startsWith('bunker://') && url.includes('?relay=')
}
  1. Ensure local signer is ready:
const localSigner = new NDKPrivateKeySigner(privateKey)
await localSigner.blockUntilReady()

const remoteSigner = new NDKNip46Signer(ndk, bunkerUrl, localSigner)
await remoteSigner.blockUntilReady()
  1. Store credentials for reconnection:
// Save for future sessions
localStorage.setItem('local-signer-key', localSigner.privateKey)
localStorage.setItem('bunker-url', bunkerUrl)

Performance Tips

Optimize Queries

// ❌ 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] })
])

Cache Profile Lookups

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
}

Limit Result Sets

// Always use limit to prevent over-fetching
const filter: NDKFilter = {
  kinds: [1],
  authors: [pubkey],
  limit: 50  // ← Important!
}

Debounce Subscription Updates

import { debounce } from 'lodash'

const debouncedUpdate = debounce((event: NDKEvent) => {
  handleEvent(event)
}, 300)

sub.on('event', debouncedUpdate)

Testing Tips

Mock NDK in Tests

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

Test Event Creation

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 reference
  • quick-reference.md - Quick lookup
  • examples/ - Code examples