# 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:**
```typescript
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):**
```typescript
// 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)
```typescript
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
```typescript
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)
```typescript
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.signer` to enable signing operations
- Use `await signer.user()` to get the authenticated user

### 3. Event Creation & Publishing

#### Basic Event Pattern
```typescript
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
```typescript
// 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`, and `tags` properties
- Optional: Set `created_at` timestamp (defaults to now)
- Call `await event.sign()` before publishing
- Call `await event.publish()` to broadcast to relays
- Access `event.id` after signing for the event hash

### 4. Querying Events with Filters

#### fetchEvents() - One-time Fetch
```typescript
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
```typescript
// 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
```typescript
// 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
```typescript
// 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
```typescript
// 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: false` for real-time monitoring
- Use `closeOnEose: true` for 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
```typescript
// 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
```typescript
// 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
```typescript
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
```typescript
// 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:

```typescript
// 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
```typescript
// 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
```typescript
// 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
```typescript
// 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
```typescript
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
```typescript
// 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
```typescript
// 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
```typescript
// 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

```typescript
// 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
```typescript
// 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
```typescript
// 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: false` for 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
```typescript
// Instead of multiple fetchEvents calls
const [products, orders, profiles] = await Promise.all([
  ndk.fetchEvents(productFilter),
  ndk.fetchEvents(orderFilter),
  ndk.fetchEvents(profileFilter),
])
```

### Limiting Results
```typescript
const filter = {
  kinds: [1],
  authors: [pubkey],
  limit: 50,  // Limit results
  since: recentTimestamp,  // Only recent events
}
```

### Caching with React Query
```typescript
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 initialization
- `src/lib/stores/auth.ts` - Authentication with NDK signers
- `src/queries/*.tsx` - Query patterns with NDK
- `src/publish/*.tsx` - Event publishing patterns
- `scripts/gen_*.ts` - Event creation examples

---

*This reference is based on NDK version used in production and real-world patterns from the Plebeian Market application.*

