Files
next.orly.dev/.claude/skills/ndk/ndk-skill.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

702 lines
17 KiB
Markdown

# 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.*