diff --git a/.claude/skills/ndk/INDEX.md b/.claude/skills/ndk/INDEX.md
new file mode 100644
index 0000000..41d6d98
--- /dev/null
+++ b/.claude/skills/ndk/INDEX.md
@@ -0,0 +1,286 @@
+# NDK (Nostr Development Kit) Claude Skill
+
+> **Comprehensive knowledge base for working with NDK in production applications**
+
+This Claude skill provides deep expertise in the Nostr Development Kit based on real-world usage patterns from the Plebeian Market application.
+
+## ๐ Documentation Structure
+
+```
+.claude/skills/ndk/
+โโโ README.md # This file - Overview and getting started
+โโโ ndk-skill.md # Complete reference guide (18KB)
+โโโ quick-reference.md # Fast lookup for common tasks (7KB)
+โโโ troubleshooting.md # Common problems and solutions
+โโโ examples/ # Production code examples
+ โโโ README.md
+ โโโ 01-initialization.ts # NDK setup and connection
+ โโโ 02-authentication.ts # NIP-07, NIP-46, private keys
+ โโโ 03-publishing-events.ts # Creating and publishing events
+ โโโ 04-querying-subscribing.ts # Fetching and real-time subs
+ โโโ 05-users-profiles.ts # User and profile management
+```
+
+## ๐ Quick Start
+
+### For Quick Lookups
+Start with **`quick-reference.md`** for:
+- Common code snippets
+- Quick syntax reminders
+- Frequently used patterns
+
+### For Deep Learning
+Read **`ndk-skill.md`** for:
+- Complete API documentation
+- Best practices
+- Integration patterns
+- Performance optimization
+
+### For Problem Solving
+Check **`troubleshooting.md`** for:
+- Common error solutions
+- Performance tips
+- Testing strategies
+- Debug techniques
+
+### For Code Examples
+Browse **`examples/`** directory for:
+- Real production code
+- Full implementations
+- React integration patterns
+- Error handling examples
+
+## ๐ Core Topics Covered
+
+### 1. Initialization & Setup
+- Basic NDK initialization
+- Multiple instance patterns (main + zap relays)
+- Connection management with timeouts
+- Relay pool configuration
+- Connection status monitoring
+
+### 2. Authentication
+- **NIP-07**: Browser extension signers (Alby, nos2x)
+- **NIP-46**: Remote signers (Bunker)
+- **Private Keys**: Direct key management
+- Auto-login with localStorage
+- Multi-account session management
+
+### 3. Event Publishing
+- Basic text notes
+- Parameterized replaceable events (products, profiles)
+- Order and payment events
+- Batch publishing
+- Error handling patterns
+
+### 4. Querying & Subscriptions
+- One-time fetches with `fetchEvents()`
+- Real-time subscriptions
+- Tag filtering patterns
+- Time-range queries
+- Event monitoring
+- React Query integration
+
+### 5. User & Profile Management
+- Fetch profiles (npub, hex, NIP-05)
+- Update user profiles
+- Follow/unfollow operations
+- Batch profile loading
+- Profile caching strategies
+
+### 6. Advanced Patterns
+- Store-based NDK management
+- Query + subscription combination
+- Event parsing utilities
+- Memory leak prevention
+- Performance optimization
+
+## ๐ฏ Use Cases
+
+### Building a Nostr Client
+```typescript
+// Initialize
+const { ndk, isConnected } = await initializeNDK({
+ relays: ['wss://relay.damus.io', 'wss://nos.lol'],
+ timeoutMs: 10000
+})
+
+// Authenticate
+const { user } = await loginWithExtension(ndk)
+
+// Publish
+await publishBasicNote(ndk, 'Hello Nostr!')
+
+// Subscribe
+const sub = subscribeToNotes(ndk, user.pubkey, (event) => {
+ console.log('New note:', event.content)
+})
+```
+
+### Building a Marketplace
+```typescript
+// Publish product
+await publishProduct(ndk, {
+ slug: 'bitcoin-shirt',
+ title: 'Bitcoin T-Shirt',
+ price: 25,
+ currency: 'USD',
+ images: ['https://...']
+})
+
+// Create order
+await createOrder(ndk, {
+ orderId: uuidv4(),
+ sellerPubkey: merchant.pubkey,
+ productRef: '30402:pubkey:bitcoin-shirt',
+ quantity: 1,
+ totalAmount: '25.00'
+})
+
+// Monitor payment
+monitorPaymentReceipt(ndk, orderId, invoiceId, (preimage) => {
+ console.log('Payment confirmed!')
+})
+```
+
+### React Integration
+```typescript
+function Feed() {
+ const ndk = useNDK()
+ const { user } = useAuth()
+
+ // Query with real-time updates
+ const { data: notes } = useNotesWithSubscription(
+ ndk,
+ user.pubkey
+ )
+
+ return (
+
+ {notes?.map(note => (
+
+ ))}
+
+ )
+}
+```
+
+## ๐ Common Patterns Quick Reference
+
+### Safe NDK Access
+```typescript
+const ndk = ndkActions.getNDK()
+if (!ndk) throw new Error('NDK not initialized')
+```
+
+### Subscription Cleanup
+```typescript
+useEffect(() => {
+ const sub = ndk.subscribe(filter, { closeOnEose: false })
+ sub.on('event', handleEvent)
+ return () => sub.stop() // Critical!
+}, [ndk])
+```
+
+### Error Handling
+```typescript
+try {
+ await event.sign()
+ await event.publish()
+} catch (error) {
+ console.error('Publishing failed:', error)
+ throw new Error('Failed to publish. Check connection.')
+}
+```
+
+### Tag Filtering
+```typescript
+// โ
Correct (note the # prefix for tag filters)
+{ kinds: [16], '#order': [orderId] }
+
+// โ Wrong
+{ kinds: [16], 'order': [orderId] }
+```
+
+## ๐ Development Tools
+
+### VS Code Integration
+These skill files work with:
+- Cursor AI for code completion
+- Claude for code assistance
+- GitHub Copilot with context
+
+### Debugging Tips
+```typescript
+// Check connection
+console.log('Connected relays:',
+ Array.from(ndk.pool?.relays.values() || [])
+ .filter(r => r.status === 1)
+ .map(r => r.url)
+)
+
+// Verify signer
+console.log('Signer:', ndk.signer)
+console.log('Active user:', ndk.activeUser)
+
+// Event inspection
+console.log('Event:', {
+ id: event.id,
+ kind: event.kind,
+ tags: event.tags,
+ sig: event.sig
+})
+```
+
+## ๐ Statistics
+
+- **Total Documentation**: ~50KB
+- **Code Examples**: 5 complete modules
+- **Patterns Documented**: 50+
+- **Common Issues Covered**: 15+
+- **Based On**: Real production code
+
+## ๐ Additional Resources
+
+### Official NDK Resources
+- **GitHub**: https://github.com/nostr-dev-kit/ndk
+- **Documentation**: https://ndk.fyi
+- **NPM**: `@nostr-dev-kit/ndk`
+
+### Nostr Protocol
+- **NIPs**: https://github.com/nostr-protocol/nips
+- **Nostr**: https://nostr.com
+
+### Related Tools
+- **TanStack Query**: React state management
+- **TanStack Router**: Type-safe routing
+- **Radix UI**: Accessible components
+
+## ๐ก Tips for Using This Skill
+
+1. **Start Small**: Begin with quick-reference.md for syntax
+2. **Go Deep**: Read ndk-skill.md section by section
+3. **Copy Examples**: Use examples/ as templates
+4. **Debug Issues**: Check troubleshooting.md first
+5. **Stay Updated**: Patterns based on production usage
+
+## ๐ค Contributing
+
+This skill is maintained based on the Plebeian Market codebase. To improve it:
+
+1. Document new patterns you discover
+2. Add solutions to common problems
+3. Update examples with better approaches
+4. Keep synchronized with NDK updates
+
+## ๐ Version Info
+
+- **Skill Version**: 1.0.0
+- **NDK Version**: Latest (based on production usage)
+- **Last Updated**: November 2025
+- **Codebase**: Plebeian Market
+
+---
+
+**Ready to build with NDK?** Start with `quick-reference.md` or dive into `examples/01-initialization.ts`!
+
diff --git a/.claude/skills/ndk/README.md b/.claude/skills/ndk/README.md
new file mode 100644
index 0000000..2a13dc4
--- /dev/null
+++ b/.claude/skills/ndk/README.md
@@ -0,0 +1,38 @@
+# NDK (Nostr Development Kit) Claude Skill
+
+This skill provides comprehensive knowledge about working with the Nostr Development Kit (NDK) library.
+
+## Files
+
+- **ndk-skill.md** - Complete reference documentation with patterns from production usage
+- **quick-reference.md** - Quick lookup guide for common NDK tasks
+- **examples/** - Code examples extracted from the Plebeian Market codebase
+
+## Usage
+
+When working with NDK-related code, reference these documents to:
+- Understand initialization patterns
+- Learn authentication flows (NIP-07, NIP-46, private keys)
+- Implement event creation and publishing
+- Set up subscriptions for real-time updates
+- Query events with filters
+- Handle users and profiles
+- Integrate with TanStack Query
+
+## Key Topics Covered
+
+1. NDK Initialization & Configuration
+2. Authentication & Signers
+3. Event Creation & Publishing
+4. Querying Events
+5. Real-time Subscriptions
+6. User & Profile Management
+7. Tag Handling
+8. Replaceable Events
+9. Relay Management
+10. Integration with React/TanStack Query
+11. Error Handling & Best Practices
+12. Performance Optimization
+
+All examples are based on real production code from the Plebeian Market application.
+
diff --git a/.claude/skills/ndk/examples/01-initialization.ts b/.claude/skills/ndk/examples/01-initialization.ts
new file mode 100644
index 0000000..bada6d6
--- /dev/null
+++ b/.claude/skills/ndk/examples/01-initialization.ts
@@ -0,0 +1,162 @@
+/**
+ * NDK Initialization Patterns
+ *
+ * Examples from: src/lib/stores/ndk.ts
+ */
+
+import NDK from '@nostr-dev-kit/ndk'
+
+// ============================================================
+// BASIC INITIALIZATION
+// ============================================================
+
+const basicInit = () => {
+ const ndk = new NDK({
+ explicitRelayUrls: ['wss://relay.damus.io', 'wss://relay.nostr.band']
+ })
+
+ return ndk
+}
+
+// ============================================================
+// PRODUCTION PATTERN - WITH MULTIPLE NDK INSTANCES
+// ============================================================
+
+const productionInit = (relays: string[], zapRelays: string[]) => {
+ // Main NDK instance for general operations
+ const ndk = new NDK({
+ explicitRelayUrls: relays
+ })
+
+ // Separate NDK for zap operations (performance optimization)
+ const zapNdk = new NDK({
+ explicitRelayUrls: zapRelays
+ })
+
+ return { ndk, zapNdk }
+}
+
+// ============================================================
+// CONNECTION WITH TIMEOUT
+// ============================================================
+
+const connectWithTimeout = async (
+ ndk: NDK,
+ timeoutMs: number = 10000
+): Promise => {
+ // Create connection promise
+ const connectPromise = ndk.connect()
+
+ // Create timeout promise
+ const timeoutPromise = new Promise((_, reject) =>
+ setTimeout(() => reject(new Error('Connection timeout')), timeoutMs)
+ )
+
+ try {
+ // Race between connection and timeout
+ await Promise.race([connectPromise, timeoutPromise])
+ console.log('โ
NDK connected successfully')
+ } catch (error) {
+ if (error instanceof Error && error.message === 'Connection timeout') {
+ console.error('โ Connection timed out after', timeoutMs, 'ms')
+ } else {
+ console.error('โ Connection failed:', error)
+ }
+ throw error
+ }
+}
+
+// ============================================================
+// FULL INITIALIZATION FLOW
+// ============================================================
+
+interface InitConfig {
+ relays?: string[]
+ zapRelays?: string[]
+ timeoutMs?: number
+}
+
+const defaultRelays = [
+ 'wss://relay.damus.io',
+ 'wss://relay.nostr.band',
+ 'wss://nos.lol'
+]
+
+const defaultZapRelays = [
+ 'wss://relay.damus.io',
+ 'wss://nostr.wine'
+]
+
+const initializeNDK = async (config: InitConfig = {}) => {
+ const {
+ relays = defaultRelays,
+ zapRelays = defaultZapRelays,
+ timeoutMs = 10000
+ } = config
+
+ // Initialize instances
+ const ndk = new NDK({ explicitRelayUrls: relays })
+ const zapNdk = new NDK({ explicitRelayUrls: zapRelays })
+
+ // Connect with timeout protection
+ try {
+ await connectWithTimeout(ndk, timeoutMs)
+ await connectWithTimeout(zapNdk, timeoutMs)
+
+ return { ndk, zapNdk, isConnected: true }
+ } catch (error) {
+ return { ndk, zapNdk, isConnected: false, error }
+ }
+}
+
+// ============================================================
+// CHECKING CONNECTION STATUS
+// ============================================================
+
+const getConnectionStatus = (ndk: NDK) => {
+ const connectedRelays = Array.from(ndk.pool?.relays.values() || [])
+ .filter(relay => relay.status === 1)
+ .map(relay => relay.url)
+
+ const isConnected = connectedRelays.length > 0
+
+ return {
+ isConnected,
+ connectedRelays,
+ totalRelays: ndk.pool?.relays.size || 0
+ }
+}
+
+// ============================================================
+// USAGE EXAMPLE
+// ============================================================
+
+async function main() {
+ // Initialize
+ const { ndk, zapNdk, isConnected } = await initializeNDK({
+ relays: defaultRelays,
+ zapRelays: defaultZapRelays,
+ timeoutMs: 10000
+ })
+
+ if (!isConnected) {
+ console.error('Failed to connect to relays')
+ return
+ }
+
+ // Check status
+ const status = getConnectionStatus(ndk)
+ console.log('Connection status:', status)
+
+ // Ready to use
+ console.log('NDK ready for operations')
+}
+
+export {
+ basicInit,
+ productionInit,
+ connectWithTimeout,
+ initializeNDK,
+ getConnectionStatus
+}
+
diff --git a/.claude/skills/ndk/examples/02-authentication.ts b/.claude/skills/ndk/examples/02-authentication.ts
new file mode 100644
index 0000000..2356205
--- /dev/null
+++ b/.claude/skills/ndk/examples/02-authentication.ts
@@ -0,0 +1,255 @@
+/**
+ * NDK Authentication Patterns
+ *
+ * Examples from: src/lib/stores/auth.ts
+ */
+
+import NDK from '@nostr-dev-kit/ndk'
+import { NDKNip07Signer, NDKPrivateKeySigner, NDKNip46Signer } from '@nostr-dev-kit/ndk'
+
+// ============================================================
+// NIP-07 - BROWSER EXTENSION SIGNER
+// ============================================================
+
+const loginWithExtension = async (ndk: NDK) => {
+ try {
+ // Create NIP-07 signer (browser extension like Alby, nos2x)
+ const signer = new NDKNip07Signer()
+
+ // Wait for signer to be ready
+ await signer.blockUntilReady()
+
+ // Set signer on NDK instance
+ ndk.signer = signer
+
+ // Get authenticated user
+ const user = await signer.user()
+
+ console.log('โ
Logged in via extension:', user.npub)
+ return { user, signer }
+ } catch (error) {
+ console.error('โ Extension login failed:', error)
+ throw new Error('Failed to login with browser extension. Is it installed?')
+ }
+}
+
+// ============================================================
+// PRIVATE KEY SIGNER
+// ============================================================
+
+const loginWithPrivateKey = async (ndk: NDK, privateKeyHex: string) => {
+ try {
+ // Validate private key format (64 hex characters)
+ if (!/^[0-9a-f]{64}$/.test(privateKeyHex)) {
+ throw new Error('Invalid private key format')
+ }
+
+ // Create private key signer
+ const signer = new NDKPrivateKeySigner(privateKeyHex)
+
+ // Wait for signer to be ready
+ await signer.blockUntilReady()
+
+ // Set signer on NDK instance
+ ndk.signer = signer
+
+ // Get authenticated user
+ const user = await signer.user()
+
+ console.log('โ
Logged in with private key:', user.npub)
+ return { user, signer }
+ } catch (error) {
+ console.error('โ Private key login failed:', error)
+ throw error
+ }
+}
+
+// ============================================================
+// NIP-46 - REMOTE SIGNER (BUNKER)
+// ============================================================
+
+const loginWithNip46 = async (
+ ndk: NDK,
+ bunkerUrl: string,
+ localPrivateKey?: string
+) => {
+ try {
+ // Create or use existing local signer
+ const localSigner = localPrivateKey
+ ? new NDKPrivateKeySigner(localPrivateKey)
+ : NDKPrivateKeySigner.generate()
+
+ // Create NIP-46 remote signer
+ const remoteSigner = new NDKNip46Signer(ndk, bunkerUrl, localSigner)
+
+ // Wait for signer to be ready (may require user approval)
+ await remoteSigner.blockUntilReady()
+
+ // Set signer on NDK instance
+ ndk.signer = remoteSigner
+
+ // Get authenticated user
+ const user = await remoteSigner.user()
+
+ console.log('โ
Logged in via NIP-46:', user.npub)
+
+ // Store local signer key for reconnection
+ return {
+ user,
+ signer: remoteSigner,
+ localSignerKey: localSigner.privateKey
+ }
+ } catch (error) {
+ console.error('โ NIP-46 login failed:', error)
+ throw error
+ }
+}
+
+// ============================================================
+// AUTO-LOGIN FROM LOCAL STORAGE
+// ============================================================
+
+const STORAGE_KEYS = {
+ AUTO_LOGIN: 'nostr:auto-login',
+ LOCAL_SIGNER: 'nostr:local-signer',
+ BUNKER_URL: 'nostr:bunker-url',
+ ENCRYPTED_KEY: 'nostr:encrypted-key'
+}
+
+const getAuthFromStorage = async (ndk: NDK) => {
+ try {
+ // Check if auto-login is enabled
+ const autoLogin = localStorage.getItem(STORAGE_KEYS.AUTO_LOGIN)
+ if (autoLogin !== 'true') {
+ return null
+ }
+
+ // Try NIP-46 bunker connection
+ const privateKey = localStorage.getItem(STORAGE_KEYS.LOCAL_SIGNER)
+ const bunkerUrl = localStorage.getItem(STORAGE_KEYS.BUNKER_URL)
+
+ if (privateKey && bunkerUrl) {
+ return await loginWithNip46(ndk, bunkerUrl, privateKey)
+ }
+
+ // Try encrypted private key
+ const encryptedKey = localStorage.getItem(STORAGE_KEYS.ENCRYPTED_KEY)
+ if (encryptedKey) {
+ // Would need decryption password from user
+ return { needsPassword: true, encryptedKey }
+ }
+
+ // Fallback to extension
+ return await loginWithExtension(ndk)
+ } catch (error) {
+ console.error('Auto-login failed:', error)
+ return null
+ }
+}
+
+// ============================================================
+// SAVE AUTH TO STORAGE
+// ============================================================
+
+const saveAuthToStorage = (
+ method: 'extension' | 'private-key' | 'nip46',
+ data?: {
+ privateKey?: string
+ bunkerUrl?: string
+ encryptedKey?: string
+ }
+) => {
+ // Enable auto-login
+ localStorage.setItem(STORAGE_KEYS.AUTO_LOGIN, 'true')
+
+ if (method === 'nip46' && data?.privateKey && data?.bunkerUrl) {
+ localStorage.setItem(STORAGE_KEYS.LOCAL_SIGNER, data.privateKey)
+ localStorage.setItem(STORAGE_KEYS.BUNKER_URL, data.bunkerUrl)
+ } else if (method === 'private-key' && data?.encryptedKey) {
+ localStorage.setItem(STORAGE_KEYS.ENCRYPTED_KEY, data.encryptedKey)
+ }
+ // Extension doesn't need storage
+}
+
+// ============================================================
+// LOGOUT
+// ============================================================
+
+const logout = (ndk: NDK) => {
+ // Remove signer from NDK
+ ndk.signer = undefined
+
+ // Clear all auth storage
+ Object.values(STORAGE_KEYS).forEach(key => {
+ localStorage.removeItem(key)
+ })
+
+ console.log('โ
Logged out successfully')
+}
+
+// ============================================================
+// GET CURRENT USER
+// ============================================================
+
+const getCurrentUser = async (ndk: NDK) => {
+ if (!ndk.signer) {
+ return null
+ }
+
+ try {
+ const user = await ndk.signer.user()
+ return {
+ pubkey: user.pubkey,
+ npub: user.npub,
+ profile: await user.fetchProfile()
+ }
+ } catch (error) {
+ console.error('Failed to get current user:', error)
+ return null
+ }
+}
+
+// ============================================================
+// USAGE EXAMPLE
+// ============================================================
+
+async function authExample(ndk: NDK) {
+ // Try auto-login first
+ let auth = await getAuthFromStorage(ndk)
+
+ if (!auth) {
+ // Manual login options
+ console.log('Choose login method:')
+ console.log('1. Browser Extension (NIP-07)')
+ console.log('2. Private Key')
+ console.log('3. Remote Signer (NIP-46)')
+
+ // Example: login with extension
+ auth = await loginWithExtension(ndk)
+ saveAuthToStorage('extension')
+ }
+
+ if (auth && 'needsPassword' in auth) {
+ // Handle encrypted key case
+ console.log('Password required for encrypted key')
+ return
+ }
+
+ // Get current user info
+ const currentUser = await getCurrentUser(ndk)
+ console.log('Current user:', currentUser)
+
+ // Logout when done
+ // logout(ndk)
+}
+
+export {
+ loginWithExtension,
+ loginWithPrivateKey,
+ loginWithNip46,
+ getAuthFromStorage,
+ saveAuthToStorage,
+ logout,
+ getCurrentUser
+}
+
diff --git a/.claude/skills/ndk/examples/03-publishing-events.ts b/.claude/skills/ndk/examples/03-publishing-events.ts
new file mode 100644
index 0000000..bd068e4
--- /dev/null
+++ b/.claude/skills/ndk/examples/03-publishing-events.ts
@@ -0,0 +1,376 @@
+/**
+ * NDK Event Publishing Patterns
+ *
+ * Examples from: src/publish/orders.tsx, scripts/gen_products.ts
+ */
+
+import NDK, { NDKEvent, NDKTag } from '@nostr-dev-kit/ndk'
+
+// ============================================================
+// BASIC EVENT PUBLISHING
+// ============================================================
+
+const publishBasicNote = async (ndk: NDK, content: string) => {
+ // Create event
+ const event = new NDKEvent(ndk)
+ event.kind = 1 // Text note
+ event.content = content
+ event.tags = []
+
+ // Sign and publish
+ await event.sign()
+ await event.publish()
+
+ console.log('โ
Published note:', event.id)
+ return event.id
+}
+
+// ============================================================
+// EVENT WITH TAGS
+// ============================================================
+
+const publishNoteWithTags = async (
+ ndk: NDK,
+ content: string,
+ options: {
+ mentions?: string[] // pubkeys to mention
+ hashtags?: string[]
+ replyTo?: string // event ID
+ }
+) => {
+ const event = new NDKEvent(ndk)
+ event.kind = 1
+ event.content = content
+ event.tags = []
+
+ // Add mentions
+ if (options.mentions) {
+ options.mentions.forEach(pubkey => {
+ event.tags.push(['p', pubkey])
+ })
+ }
+
+ // Add hashtags
+ if (options.hashtags) {
+ options.hashtags.forEach(tag => {
+ event.tags.push(['t', tag])
+ })
+ }
+
+ // Add reply
+ if (options.replyTo) {
+ event.tags.push(['e', options.replyTo, '', 'reply'])
+ }
+
+ await event.sign()
+ await event.publish()
+
+ return event.id
+}
+
+// ============================================================
+// PRODUCT LISTING (PARAMETERIZED REPLACEABLE EVENT)
+// ============================================================
+
+interface ProductData {
+ slug: string // Unique identifier
+ title: string
+ description: string
+ price: number
+ currency: string
+ images: string[]
+ shippingRefs?: string[]
+ category?: string
+}
+
+const publishProduct = async (ndk: NDK, product: ProductData) => {
+ const event = new NDKEvent(ndk)
+ event.kind = 30402 // Product listing kind
+ event.content = product.description
+
+ // Build tags
+ event.tags = [
+ ['d', product.slug], // Unique identifier (required for replaceable)
+ ['title', product.title],
+ ['price', product.price.toString(), product.currency],
+ ]
+
+ // Add images
+ product.images.forEach(image => {
+ event.tags.push(['image', image])
+ })
+
+ // Add shipping options
+ if (product.shippingRefs) {
+ product.shippingRefs.forEach(ref => {
+ event.tags.push(['shipping', ref])
+ })
+ }
+
+ // Add category
+ if (product.category) {
+ event.tags.push(['t', product.category])
+ }
+
+ // Optional: set custom timestamp
+ event.created_at = Math.floor(Date.now() / 1000)
+
+ await event.sign()
+ await event.publish()
+
+ console.log('โ
Published product:', product.title)
+ return event.id
+}
+
+// ============================================================
+// ORDER CREATION EVENT
+// ============================================================
+
+interface OrderData {
+ orderId: string
+ sellerPubkey: string
+ productRef: string
+ quantity: number
+ totalAmount: string
+ currency: string
+ shippingRef?: string
+ shippingAddress?: string
+ email?: string
+ phone?: string
+ notes?: string
+}
+
+const createOrder = async (ndk: NDK, order: OrderData) => {
+ const event = new NDKEvent(ndk)
+ event.kind = 16 // Order processing kind
+ event.content = order.notes || ''
+
+ // Required tags per spec
+ event.tags = [
+ ['p', order.sellerPubkey],
+ ['subject', `Order ${order.orderId.substring(0, 8)}`],
+ ['type', 'order-creation'],
+ ['order', order.orderId],
+ ['amount', order.totalAmount],
+ ['item', order.productRef, order.quantity.toString()],
+ ]
+
+ // Optional tags
+ if (order.shippingRef) {
+ event.tags.push(['shipping', order.shippingRef])
+ }
+
+ if (order.shippingAddress) {
+ event.tags.push(['address', order.shippingAddress])
+ }
+
+ if (order.email) {
+ event.tags.push(['email', order.email])
+ }
+
+ if (order.phone) {
+ event.tags.push(['phone', order.phone])
+ }
+
+ try {
+ await event.sign()
+ await event.publish()
+
+ console.log('โ
Order created:', order.orderId)
+ return { success: true, eventId: event.id }
+ } catch (error) {
+ console.error('โ Failed to create order:', error)
+ return { success: false, error }
+ }
+}
+
+// ============================================================
+// STATUS UPDATE EVENT
+// ============================================================
+
+const publishStatusUpdate = async (
+ ndk: NDK,
+ orderId: string,
+ recipientPubkey: string,
+ status: 'pending' | 'paid' | 'shipped' | 'delivered' | 'cancelled',
+ notes?: string
+) => {
+ const event = new NDKEvent(ndk)
+ event.kind = 16
+ event.content = notes || `Order status updated to ${status}`
+ event.tags = [
+ ['p', recipientPubkey],
+ ['subject', 'order-info'],
+ ['type', 'status-update'],
+ ['order', orderId],
+ ['status', status],
+ ]
+
+ await event.sign()
+ await event.publish()
+
+ return event.id
+}
+
+// ============================================================
+// BATCH PUBLISHING
+// ============================================================
+
+const publishMultipleEvents = async (
+ ndk: NDK,
+ events: Array<{ kind: number; content: string; tags: NDKTag[] }>
+) => {
+ const results = []
+
+ for (const eventData of events) {
+ try {
+ const event = new NDKEvent(ndk)
+ event.kind = eventData.kind
+ event.content = eventData.content
+ event.tags = eventData.tags
+
+ await event.sign()
+ await event.publish()
+
+ results.push({ success: true, eventId: event.id })
+ } catch (error) {
+ results.push({ success: false, error })
+ }
+ }
+
+ return results
+}
+
+// ============================================================
+// PUBLISH WITH CUSTOM SIGNER
+// ============================================================
+
+import { NDKSigner } from '@nostr-dev-kit/ndk'
+
+const publishWithCustomSigner = async (
+ ndk: NDK,
+ signer: NDKSigner,
+ eventData: { kind: number; content: string; tags: NDKTag[] }
+) => {
+ const event = new NDKEvent(ndk)
+ event.kind = eventData.kind
+ event.content = eventData.content
+ event.tags = eventData.tags
+
+ // Sign with specific signer (not ndk.signer)
+ await event.sign(signer)
+ await event.publish()
+
+ return event.id
+}
+
+// ============================================================
+// ERROR HANDLING PATTERN
+// ============================================================
+
+const publishWithErrorHandling = async (
+ ndk: NDK,
+ eventData: { kind: number; content: string; tags: NDKTag[] }
+) => {
+ // Validate NDK
+ if (!ndk) {
+ throw new Error('NDK not initialized')
+ }
+
+ // Validate signer
+ if (!ndk.signer) {
+ throw new Error('No active signer. Please login first.')
+ }
+
+ try {
+ const event = new NDKEvent(ndk)
+ event.kind = eventData.kind
+ event.content = eventData.content
+ event.tags = eventData.tags
+
+ // Sign
+ await event.sign()
+
+ // Verify signature
+ if (!event.sig) {
+ throw new Error('Event signing failed')
+ }
+
+ // Publish
+ await event.publish()
+
+ // Verify event ID
+ if (!event.id) {
+ throw new Error('Event ID not generated')
+ }
+
+ return {
+ success: true,
+ eventId: event.id,
+ pubkey: event.pubkey
+ }
+ } catch (error) {
+ console.error('Publishing failed:', error)
+
+ if (error instanceof Error) {
+ // Handle specific error types
+ if (error.message.includes('relay')) {
+ throw new Error('Failed to publish to relays. Check connection.')
+ }
+ if (error.message.includes('sign')) {
+ throw new Error('Failed to sign event. Check signer.')
+ }
+ }
+
+ throw error
+ }
+}
+
+// ============================================================
+// USAGE EXAMPLE
+// ============================================================
+
+async function publishingExample(ndk: NDK) {
+ // Simple note
+ await publishBasicNote(ndk, 'Hello Nostr!')
+
+ // Note with tags
+ await publishNoteWithTags(ndk, 'Check out this product!', {
+ hashtags: ['marketplace', 'nostr'],
+ mentions: ['pubkey123...']
+ })
+
+ // Product listing
+ await publishProduct(ndk, {
+ slug: 'bitcoin-tshirt',
+ title: 'Bitcoin T-Shirt',
+ description: 'High quality Bitcoin t-shirt',
+ price: 25,
+ currency: 'USD',
+ images: ['https://example.com/image.jpg'],
+ category: 'clothing'
+ })
+
+ // Order
+ await createOrder(ndk, {
+ orderId: 'order-123',
+ sellerPubkey: 'seller-pubkey',
+ productRef: '30402:pubkey:bitcoin-tshirt',
+ quantity: 1,
+ totalAmount: '25.00',
+ currency: 'USD',
+ email: 'customer@example.com'
+ })
+}
+
+export {
+ publishBasicNote,
+ publishNoteWithTags,
+ publishProduct,
+ createOrder,
+ publishStatusUpdate,
+ publishMultipleEvents,
+ publishWithCustomSigner,
+ publishWithErrorHandling
+}
+
diff --git a/.claude/skills/ndk/examples/04-querying-subscribing.ts b/.claude/skills/ndk/examples/04-querying-subscribing.ts
new file mode 100644
index 0000000..ff75e97
--- /dev/null
+++ b/.claude/skills/ndk/examples/04-querying-subscribing.ts
@@ -0,0 +1,404 @@
+/**
+ * NDK Query and Subscription Patterns
+ *
+ * Examples from: src/queries/orders.tsx, src/queries/payment.tsx
+ */
+
+import NDK, { NDKEvent, NDKFilter, NDKSubscription } from '@nostr-dev-kit/ndk'
+
+// ============================================================
+// BASIC FETCH (ONE-TIME QUERY)
+// ============================================================
+
+const fetchNotes = async (ndk: NDK, authorPubkey: string, limit: number = 50) => {
+ const filter: NDKFilter = {
+ kinds: [1], // Text notes
+ authors: [authorPubkey],
+ limit
+ }
+
+ // Fetch returns a Set
+ const events = await ndk.fetchEvents(filter)
+
+ // Convert to array and sort by timestamp
+ const eventArray = Array.from(events).sort((a, b) =>
+ (b.created_at || 0) - (a.created_at || 0)
+ )
+
+ return eventArray
+}
+
+// ============================================================
+// FETCH WITH MULTIPLE FILTERS
+// ============================================================
+
+const fetchProductsByMultipleAuthors = async (
+ ndk: NDK,
+ pubkeys: string[]
+) => {
+ const filter: NDKFilter = {
+ kinds: [30402], // Product listings
+ authors: pubkeys,
+ limit: 100
+ }
+
+ const events = await ndk.fetchEvents(filter)
+ return Array.from(events)
+}
+
+// ============================================================
+// FETCH WITH TAG FILTERS
+// ============================================================
+
+const fetchOrderEvents = async (ndk: NDK, orderId: string) => {
+ const filter: NDKFilter = {
+ kinds: [16, 17], // Order and payment receipt
+ '#order': [orderId], // Tag filter (note the # prefix)
+ }
+
+ const events = await ndk.fetchEvents(filter)
+ return Array.from(events)
+}
+
+// ============================================================
+// FETCH WITH TIME RANGE
+// ============================================================
+
+const fetchRecentEvents = async (
+ ndk: NDK,
+ kind: number,
+ hoursAgo: number = 24
+) => {
+ const now = Math.floor(Date.now() / 1000)
+ const since = now - (hoursAgo * 3600)
+
+ const filter: NDKFilter = {
+ kinds: [kind],
+ since,
+ until: now,
+ limit: 100
+ }
+
+ const events = await ndk.fetchEvents(filter)
+ return Array.from(events)
+}
+
+// ============================================================
+// FETCH BY EVENT ID
+// ============================================================
+
+const fetchEventById = async (ndk: NDK, eventId: string) => {
+ const filter: NDKFilter = {
+ ids: [eventId]
+ }
+
+ const events = await ndk.fetchEvents(filter)
+
+ if (events.size === 0) {
+ return null
+ }
+
+ return Array.from(events)[0]
+}
+
+// ============================================================
+// BASIC SUBSCRIPTION (REAL-TIME)
+// ============================================================
+
+const subscribeToNotes = (
+ ndk: NDK,
+ authorPubkey: string,
+ onEvent: (event: NDKEvent) => void
+): NDKSubscription => {
+ const filter: NDKFilter = {
+ kinds: [1],
+ authors: [authorPubkey]
+ }
+
+ const subscription = ndk.subscribe(filter, {
+ closeOnEose: false // Keep open for real-time updates
+ })
+
+ // Event handler
+ subscription.on('event', (event: NDKEvent) => {
+ onEvent(event)
+ })
+
+ // EOSE (End of Stored Events) handler
+ subscription.on('eose', () => {
+ console.log('โ
Received all stored events')
+ })
+
+ return subscription
+}
+
+// ============================================================
+// SUBSCRIPTION WITH CLEANUP
+// ============================================================
+
+const createManagedSubscription = (
+ ndk: NDK,
+ filter: NDKFilter,
+ handlers: {
+ onEvent: (event: NDKEvent) => void
+ onEose?: () => void
+ onClose?: () => void
+ }
+) => {
+ const subscription = ndk.subscribe(filter, { closeOnEose: false })
+
+ subscription.on('event', handlers.onEvent)
+
+ if (handlers.onEose) {
+ subscription.on('eose', handlers.onEose)
+ }
+
+ if (handlers.onClose) {
+ subscription.on('close', handlers.onClose)
+ }
+
+ // Return cleanup function
+ return () => {
+ subscription.stop()
+ console.log('โ
Subscription stopped')
+ }
+}
+
+// ============================================================
+// MONITORING SPECIFIC EVENT
+// ============================================================
+
+const monitorPaymentReceipt = (
+ ndk: NDK,
+ orderId: string,
+ invoiceId: string,
+ onPaymentReceived: (preimage: string) => void
+): NDKSubscription => {
+ const sessionStart = Math.floor(Date.now() / 1000)
+
+ const filter: NDKFilter = {
+ kinds: [17], // Payment receipt
+ '#order': [orderId],
+ '#payment-request': [invoiceId],
+ since: sessionStart - 30 // 30 second buffer for clock skew
+ }
+
+ const subscription = ndk.subscribe(filter, { closeOnEose: false })
+
+ subscription.on('event', (event: NDKEvent) => {
+ // Verify event is recent
+ if (event.created_at && event.created_at < sessionStart - 30) {
+ console.log('โฐ Ignoring old receipt')
+ return
+ }
+
+ // Verify it's the correct invoice
+ const paymentRequestTag = event.tags.find(tag => tag[0] === 'payment-request')
+ if (paymentRequestTag?.[1] !== invoiceId) {
+ return
+ }
+
+ // Extract preimage
+ const paymentTag = event.tags.find(tag => tag[0] === 'payment')
+ const preimage = paymentTag?.[3] || 'external-payment'
+
+ console.log('โ
Payment received!')
+ subscription.stop()
+ onPaymentReceived(preimage)
+ })
+
+ return subscription
+}
+
+// ============================================================
+// REACT INTEGRATION PATTERN
+// ============================================================
+
+import { useEffect, useState } from 'react'
+
+function useOrderSubscription(ndk: NDK | null, orderId: string) {
+ const [events, setEvents] = useState([])
+ const [eosed, setEosed] = useState(false)
+
+ useEffect(() => {
+ if (!ndk || !orderId) return
+
+ const filter: NDKFilter = {
+ kinds: [16, 17],
+ '#order': [orderId]
+ }
+
+ const subscription = ndk.subscribe(filter, { closeOnEose: false })
+
+ subscription.on('event', (event: NDKEvent) => {
+ setEvents(prev => {
+ // Avoid duplicates
+ if (prev.some(e => e.id === event.id)) {
+ return prev
+ }
+ return [...prev, event].sort((a, b) =>
+ (a.created_at || 0) - (b.created_at || 0)
+ )
+ })
+ })
+
+ subscription.on('eose', () => {
+ setEosed(true)
+ })
+
+ // Cleanup on unmount
+ return () => {
+ subscription.stop()
+ }
+ }, [ndk, orderId])
+
+ return { events, eosed }
+}
+
+// ============================================================
+// REACT QUERY INTEGRATION
+// ============================================================
+
+import { useQuery, useQueryClient } from '@tanstack/react-query'
+
+// Query function
+const fetchProducts = async (ndk: NDK, pubkey: string) => {
+ 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)
+}
+
+// Hook with subscription for real-time updates
+function useProductsWithSubscription(ndk: NDK | null, pubkey: string) {
+ const queryClient = useQueryClient()
+
+ // Initial query
+ const query = useQuery({
+ queryKey: ['products', pubkey],
+ queryFn: () => fetchProducts(ndk!, pubkey),
+ enabled: !!ndk && !!pubkey,
+ staleTime: 30000
+ })
+
+ // Real-time subscription
+ useEffect(() => {
+ if (!ndk || !pubkey) return
+
+ const filter: NDKFilter = {
+ kinds: [30402],
+ authors: [pubkey]
+ }
+
+ const subscription = ndk.subscribe(filter, { closeOnEose: false })
+
+ subscription.on('event', () => {
+ // Invalidate query to trigger refetch
+ queryClient.invalidateQueries({ queryKey: ['products', pubkey] })
+ })
+
+ return () => {
+ subscription.stop()
+ }
+ }, [ndk, pubkey, queryClient])
+
+ return query
+}
+
+// ============================================================
+// ADVANCED: WAITING FOR SPECIFIC EVENT
+// ============================================================
+
+const waitForEvent = (
+ ndk: NDK,
+ filter: NDKFilter,
+ condition: (event: NDKEvent) => boolean,
+ timeoutMs: number = 30000
+): Promise => {
+ return new Promise((resolve) => {
+ const subscription = ndk.subscribe(filter, { closeOnEose: false })
+
+ // Timeout
+ const timeout = setTimeout(() => {
+ subscription.stop()
+ resolve(null)
+ }, timeoutMs)
+
+ // Event handler
+ subscription.on('event', (event: NDKEvent) => {
+ if (condition(event)) {
+ clearTimeout(timeout)
+ subscription.stop()
+ resolve(event)
+ }
+ })
+ })
+}
+
+// Usage example
+async function waitForPayment(ndk: NDK, orderId: string, invoiceId: string) {
+ const paymentEvent = await waitForEvent(
+ ndk,
+ {
+ kinds: [17],
+ '#order': [orderId],
+ since: Math.floor(Date.now() / 1000)
+ },
+ (event) => {
+ const tag = event.tags.find(t => t[0] === 'payment-request')
+ return tag?.[1] === invoiceId
+ },
+ 60000 // 60 second timeout
+ )
+
+ if (paymentEvent) {
+ console.log('โ
Payment confirmed!')
+ return paymentEvent
+ } else {
+ console.log('โฐ Payment timeout')
+ return null
+ }
+}
+
+// ============================================================
+// USAGE EXAMPLES
+// ============================================================
+
+async function queryExample(ndk: NDK) {
+ // Fetch notes
+ const notes = await fetchNotes(ndk, 'pubkey123', 50)
+ console.log(`Found ${notes.length} notes`)
+
+ // Subscribe to new notes
+ const cleanup = subscribeToNotes(ndk, 'pubkey123', (event) => {
+ console.log('New note:', event.content)
+ })
+
+ // Clean up after 60 seconds
+ setTimeout(cleanup, 60000)
+
+ // Monitor payment
+ monitorPaymentReceipt(ndk, 'order-123', 'invoice-456', (preimage) => {
+ console.log('Payment received:', preimage)
+ })
+}
+
+export {
+ fetchNotes,
+ fetchProductsByMultipleAuthors,
+ fetchOrderEvents,
+ fetchRecentEvents,
+ fetchEventById,
+ subscribeToNotes,
+ createManagedSubscription,
+ monitorPaymentReceipt,
+ useOrderSubscription,
+ useProductsWithSubscription,
+ waitForEvent
+}
+
diff --git a/.claude/skills/ndk/examples/05-users-profiles.ts b/.claude/skills/ndk/examples/05-users-profiles.ts
new file mode 100644
index 0000000..3a9beb6
--- /dev/null
+++ b/.claude/skills/ndk/examples/05-users-profiles.ts
@@ -0,0 +1,423 @@
+/**
+ * NDK User and Profile Handling
+ *
+ * Examples from: src/queries/profiles.tsx, src/components/Profile.tsx
+ */
+
+import NDK, { NDKUser, NDKUserProfile } from '@nostr-dev-kit/ndk'
+import { nip19 } from 'nostr-tools'
+
+// ============================================================
+// FETCH PROFILE BY NPUB
+// ============================================================
+
+const fetchProfileByNpub = async (ndk: NDK, npub: string): Promise => {
+ try {
+ // Get user object from npub
+ const user = ndk.getUser({ npub })
+
+ // Fetch profile from relays
+ const profile = await user.fetchProfile()
+
+ return profile
+ } catch (error) {
+ console.error('Failed to fetch profile:', error)
+ return null
+ }
+}
+
+// ============================================================
+// FETCH PROFILE BY HEX PUBKEY
+// ============================================================
+
+const fetchProfileByPubkey = async (ndk: NDK, pubkey: string): Promise => {
+ try {
+ const user = ndk.getUser({ hexpubkey: pubkey })
+ const profile = await user.fetchProfile()
+
+ return profile
+ } catch (error) {
+ console.error('Failed to fetch profile:', error)
+ return null
+ }
+}
+
+// ============================================================
+// FETCH PROFILE BY NIP-05
+// ============================================================
+
+const fetchProfileByNip05 = async (ndk: NDK, nip05: string): Promise => {
+ try {
+ // Resolve NIP-05 identifier to user
+ const user = await ndk.getUserFromNip05(nip05)
+
+ if (!user) {
+ console.log('User not found for NIP-05:', nip05)
+ return null
+ }
+
+ // Fetch profile
+ const profile = await user.fetchProfile()
+
+ return profile
+ } catch (error) {
+ console.error('Failed to fetch profile by NIP-05:', error)
+ return null
+ }
+}
+
+// ============================================================
+// FETCH PROFILE BY ANY IDENTIFIER
+// ============================================================
+
+const fetchProfileByIdentifier = async (
+ ndk: NDK,
+ identifier: string
+): Promise<{ profile: NDKUserProfile | null; user: NDKUser | null }> => {
+ try {
+ // Check if it's a NIP-05 (contains @)
+ if (identifier.includes('@')) {
+ const user = await ndk.getUserFromNip05(identifier)
+ if (!user) return { profile: null, user: null }
+
+ const profile = await user.fetchProfile()
+ return { profile, user }
+ }
+
+ // Check if it's an npub
+ if (identifier.startsWith('npub')) {
+ const user = ndk.getUser({ npub: identifier })
+ const profile = await user.fetchProfile()
+ return { profile, user }
+ }
+
+ // Assume it's a hex pubkey
+ const user = ndk.getUser({ hexpubkey: identifier })
+ const profile = await user.fetchProfile()
+ return { profile, user }
+ } catch (error) {
+ console.error('Failed to fetch profile:', error)
+ return { profile: null, user: null }
+ }
+}
+
+// ============================================================
+// GET CURRENT USER
+// ============================================================
+
+const getCurrentUser = async (ndk: NDK): Promise => {
+ if (!ndk.signer) {
+ console.log('No signer set')
+ return null
+ }
+
+ try {
+ const user = await ndk.signer.user()
+ return user
+ } catch (error) {
+ console.error('Failed to get current user:', error)
+ return null
+ }
+}
+
+// ============================================================
+// PROFILE DATA STRUCTURE
+// ============================================================
+
+interface ProfileData {
+ // Standard fields
+ name?: string
+ displayName?: string
+ display_name?: string
+ picture?: string
+ image?: string
+ banner?: string
+ about?: string
+
+ // Contact
+ nip05?: string
+ lud06?: string // LNURL
+ lud16?: string // Lightning address
+
+ // Social
+ website?: string
+
+ // Raw data
+ [key: string]: any
+}
+
+// ============================================================
+// EXTRACT PROFILE INFO
+// ============================================================
+
+const extractProfileInfo = (profile: NDKUserProfile | null) => {
+ if (!profile) {
+ return {
+ displayName: 'Anonymous',
+ avatar: null,
+ bio: null,
+ lightningAddress: null,
+ nip05: null
+ }
+ }
+
+ return {
+ displayName: profile.displayName || profile.display_name || profile.name || 'Anonymous',
+ avatar: profile.picture || profile.image || null,
+ banner: profile.banner || null,
+ bio: profile.about || null,
+ lightningAddress: profile.lud16 || profile.lud06 || null,
+ nip05: profile.nip05 || null,
+ website: profile.website || null
+ }
+}
+
+// ============================================================
+// UPDATE PROFILE
+// ============================================================
+
+import { NDKEvent } from '@nostr-dev-kit/ndk'
+
+const updateProfile = async (ndk: NDK, profileData: Partial) => {
+ if (!ndk.signer) {
+ throw new Error('No signer available')
+ }
+
+ // Get current profile
+ const currentUser = await ndk.signer.user()
+ const currentProfile = await currentUser.fetchProfile()
+
+ // Merge with new data
+ const updatedProfile = {
+ ...currentProfile,
+ ...profileData
+ }
+
+ // Create kind 0 (metadata) event
+ const event = new NDKEvent(ndk)
+ event.kind = 0
+ event.content = JSON.stringify(updatedProfile)
+ event.tags = []
+
+ await event.sign()
+ await event.publish()
+
+ console.log('โ
Profile updated')
+ return event.id
+}
+
+// ============================================================
+// BATCH FETCH PROFILES
+// ============================================================
+
+const fetchMultipleProfiles = async (
+ ndk: NDK,
+ pubkeys: string[]
+): Promise