# Common Nostr Implementation Mistakes and How to Avoid Them This document highlights frequent errors made when implementing Nostr clients and relays, along with solutions. ## Event Creation and Signing ### Mistake 1: Incorrect Event ID Calculation **Problem**: Wrong serialization order or missing fields when calculating SHA256. **Correct Serialization**: ```json [ 0, // Must be integer 0 , // Lowercase hex string , // Unix timestamp integer , // Integer , // Array of arrays // String ] ``` **Common errors**: - Using string "0" instead of integer 0 - Including `id` or `sig` fields in serialization - Wrong field order - Not using compact JSON (no spaces) - Using uppercase hex **Fix**: Serialize exactly as shown, compact JSON, SHA256 the UTF-8 bytes. ### Mistake 2: Wrong Signature Algorithm **Problem**: Using ECDSA instead of Schnorr signatures. **Correct**: - Use Schnorr signatures (BIP-340) - Curve: secp256k1 - Sign the 32-byte event ID **Libraries**: - JavaScript: noble-secp256k1 - Rust: secp256k1 - Go: btcsuite/btcd/btcec/v2/schnorr - Python: secp256k1-py ### Mistake 3: Invalid created_at Timestamps **Problem**: Events with far-future timestamps or very old timestamps. **Best practices**: - Use current Unix time: `Math.floor(Date.now() / 1000)` - Relays often reject if `created_at > now + 15 minutes` - Don't backdate events to manipulate ordering **Fix**: Always use current time when creating events. ### Mistake 4: Malformed Tags **Problem**: Tags that aren't arrays or have wrong structure. **Correct format**: ```json { "tags": [ ["e", "event-id", "relay-url", "marker"], ["p", "pubkey", "relay-url"], ["t", "hashtag"] ] } ``` **Common errors**: - Using objects instead of arrays: `{"e": "..."}` ❌ - Missing inner arrays: `["e", "event-id"]` when nested in tags is wrong - Wrong nesting depth - Non-string values (except for specific NIPs) ### Mistake 5: Not Handling Replaceable Events **Problem**: Showing multiple versions of replaceable events. **Event types**: - **Replaceable (10000-19999)**: Same author + kind → replace - **Parameterized Replaceable (30000-39999)**: Same author + kind + d-tag → replace **Fix**: ```javascript // For replaceable events const key = `${event.pubkey}:${event.kind}` if (latestEvents[key]?.created_at < event.created_at) { latestEvents[key] = event } // For parameterized replaceable events const dTag = event.tags.find(t => t[0] === 'd')?.[1] || '' const key = `${event.pubkey}:${event.kind}:${dTag}` if (latestEvents[key]?.created_at < event.created_at) { latestEvents[key] = event } ``` ## WebSocket Communication ### Mistake 6: Not Handling EOSE **Problem**: Loading indicators never finish or show wrong state. **Solution**: ```javascript const receivedEvents = new Set() let eoseReceived = false ws.onmessage = (msg) => { const [type, ...rest] = JSON.parse(msg.data) if (type === 'EVENT') { const [subId, event] = rest receivedEvents.add(event.id) displayEvent(event) } if (type === 'EOSE') { eoseReceived = true hideLoadingSpinner() } } ``` ### Mistake 7: Not Closing Subscriptions **Problem**: Memory leaks and wasted bandwidth from unclosed subscriptions. **Fix**: Always send CLOSE when done: ```javascript ws.send(JSON.stringify(['CLOSE', subId])) ``` **Best practices**: - Close when component unmounts - Close before opening new subscription with same ID - Use unique subscription IDs - Track active subscriptions ### Mistake 8: Ignoring OK Messages **Problem**: Not knowing if events were accepted or rejected. **Solution**: ```javascript ws.onmessage = (msg) => { const [type, eventId, accepted, message] = JSON.parse(msg.data) if (type === 'OK') { if (!accepted) { console.error(`Event ${eventId} rejected: ${message}`) handleRejection(eventId, message) } } } ``` **Common rejection reasons**: - `pow:` - Insufficient proof of work - `blocked:` - Pubkey or content blocked - `rate-limited:` - Too many requests - `invalid:` - Failed validation ### Mistake 9: Sending Events Before WebSocket Ready **Problem**: Events lost because WebSocket not connected. **Fix**: ```javascript const sendWhenReady = (ws, message) => { if (ws.readyState === WebSocket.OPEN) { ws.send(message) } else { ws.addEventListener('open', () => ws.send(message), { once: true }) } } ``` ### Mistake 10: Not Handling WebSocket Disconnections **Problem**: App breaks when relay goes offline. **Solution**: Implement reconnection with exponential backoff: ```javascript let reconnectDelay = 1000 const maxDelay = 30000 const connect = () => { const ws = new WebSocket(relayUrl) ws.onclose = () => { setTimeout(() => { reconnectDelay = Math.min(reconnectDelay * 2, maxDelay) connect() }, reconnectDelay) } ws.onopen = () => { reconnectDelay = 1000 // Reset on successful connection resubscribe() // Re-establish subscriptions } } ``` ## Filter Queries ### Mistake 11: Overly Broad Filters **Problem**: Requesting too many events, overwhelming relay and client. **Bad**: ```json { "kinds": [1], "limit": 10000 } ``` **Good**: ```json { "kinds": [1], "authors": [""], "limit": 50, "since": 1234567890 } ``` **Best practices**: - Always set reasonable `limit` (50-500) - Filter by `authors` when possible - Use `since`/`until` for time ranges - Be specific with `kinds` - Multiple smaller queries > one huge query ### Mistake 12: Not Using Prefix Matching **Problem**: Full hex strings in filters unnecessarily. **Optimization**: ```json { "ids": ["abc12345"], // 8 chars enough for uniqueness "authors": ["def67890"] } ``` Relays support prefix matching for `ids` and `authors`. ### Mistake 13: Duplicate Filter Fields **Problem**: Redundant filter conditions. **Bad**: ```json { "authors": ["pubkey1", "pubkey1"], "kinds": [1, 1] } ``` **Good**: ```json { "authors": ["pubkey1"], "kinds": [1] } ``` Deduplicate filter arrays. ## Threading and References ### Mistake 14: Incorrect Thread Structure **Problem**: Missing root/reply markers or wrong tag order. **Correct reply structure** (NIP-10): ```json { "kind": 1, "tags": [ ["e", "", "", "root"], ["e", "", "", "reply"], ["p", ""], ["p", ""] ] } ``` **Key points**: - Root event should have "root" marker - Direct parent should have "reply" marker - Include `p` tags for all mentioned users - Relay hints are optional but helpful ### Mistake 15: Missing p Tags in Replies **Problem**: Authors not notified of replies. **Fix**: Always add `p` tag for: - Original author - Authors mentioned in content - Authors in the thread chain ```json { "tags": [ ["e", "event-id", "", "reply"], ["p", "original-author"], ["p", "mentioned-user1"], ["p", "mentioned-user2"] ] } ``` ### Mistake 16: Not Using Markers **Problem**: Ambiguous thread structure. **Solution**: Always use markers in `e` tags: - `root` - Root of thread - `reply` - Direct parent - `mention` - Referenced but not replied to Without markers, clients must guess thread structure. ## Relay Management ### Mistake 17: Relying on Single Relay **Problem**: Single point of failure, censorship vulnerability. **Solution**: Connect to multiple relays (5-15 common): ```javascript const relays = [ 'wss://relay1.com', 'wss://relay2.com', 'wss://relay3.com' ] const connections = relays.map(url => connect(url)) ``` **Best practices**: - Publish to 3-5 write relays - Read from 5-10 read relays - Use NIP-65 for user's preferred relays - Fall back to NIP-05 relays - Implement relay rotation on failure ### Mistake 18: Not Implementing NIP-65 **Problem**: Querying wrong relays, missing user's events. **Correct flow**: 1. Fetch user's kind `10002` event (relay list) 2. Connect to their read relays to fetch their content 3. Connect to their write relays to send them messages ```javascript async function getUserRelays(pubkey) { // Fetch kind 10002 const relayList = await fetchEvent({ kinds: [10002], authors: [pubkey] }) const readRelays = [] const writeRelays = [] relayList.tags.forEach(([tag, url, mode]) => { if (tag === 'r') { if (!mode || mode === 'read') readRelays.push(url) if (!mode || mode === 'write') writeRelays.push(url) } }) return { readRelays, writeRelays } } ``` ### Mistake 19: Not Respecting Relay Limitations **Problem**: Violating relay policies, getting rate limited or banned. **Solution**: Fetch and respect NIP-11 relay info: ```javascript const getRelayInfo = async (relayUrl) => { const url = relayUrl.replace('wss://', 'https://').replace('ws://', 'http://') const response = await fetch(url, { headers: { 'Accept': 'application/nostr+json' } }) return response.json() } // Respect limitations const info = await getRelayInfo(relayUrl) const maxLimit = info.limitation?.max_limit || 500 const maxFilters = info.limitation?.max_filters || 10 ``` ## Security ### Mistake 20: Exposing Private Keys **Problem**: Including nsec in client code, logs, or network requests. **Never**: - Store nsec in localStorage without encryption - Log private keys - Send nsec over network - Display nsec to user unless explicitly requested - Hard-code private keys **Best practices**: - Use NIP-07 (browser extension) when possible - Encrypt keys at rest - Use NIP-46 (remote signing) for web apps - Warn users when showing nsec ### Mistake 21: Not Verifying Signatures **Problem**: Accepting invalid events, vulnerability to attacks. **Always verify**: ```javascript const verifyEvent = (event) => { // 1. Verify ID const calculatedId = sha256(serializeEvent(event)) if (calculatedId !== event.id) return false // 2. Verify signature const signatureValid = schnorr.verify( event.sig, event.id, event.pubkey ) if (!signatureValid) return false // 3. Check timestamp const now = Math.floor(Date.now() / 1000) if (event.created_at > now + 900) return false // 15 min future return true } ``` **Verify before**: - Displaying to user - Storing in database - Using event data for logic ### Mistake 22: Using NIP-04 Encryption **Problem**: Weak encryption, vulnerable to attacks. **Solution**: Use NIP-44 instead: - Modern authenticated encryption - ChaCha20-Poly1305 AEAD - Proper key derivation - Version byte for upgradability **Migration**: Update to NIP-44 for all new encrypted messages. ### Mistake 23: Not Sanitizing Content **Problem**: XSS vulnerabilities in displayed content. **Solution**: Sanitize before rendering: ```javascript import DOMPurify from 'dompurify' const safeContent = DOMPurify.sanitize(event.content, { ALLOWED_TAGS: ['b', 'i', 'u', 'a', 'code', 'pre'], ALLOWED_ATTR: ['href', 'target', 'rel'] }) ``` **Especially critical for**: - Markdown rendering - Link parsing - Image URLs - User-provided HTML ## User Experience ### Mistake 24: Not Caching Events **Problem**: Re-fetching same events repeatedly, poor performance. **Solution**: Implement event cache: ```javascript const eventCache = new Map() const cacheEvent = (event) => { eventCache.set(event.id, event) } const getCachedEvent = (eventId) => { return eventCache.get(eventId) } ``` **Cache strategies**: - LRU eviction for memory management - IndexedDB for persistence - Invalidate replaceable events on update - Cache metadata (kind 0) aggressively ### Mistake 25: Not Implementing Optimistic UI **Problem**: Slow feeling app, waiting for relay confirmation. **Solution**: Show user's events immediately: ```javascript const publishEvent = async (event) => { // Immediately show to user displayEvent(event, { pending: true }) // Publish to relays const results = await Promise.all( relays.map(relay => relay.publish(event)) ) // Update status based on results const success = results.some(r => r.accepted) displayEvent(event, { pending: false, success }) } ``` ### Mistake 26: Poor Loading States **Problem**: User doesn't know if app is working. **Solution**: Clear loading indicators: - Show spinner until EOSE - Display "Loading..." placeholder - Show how many relays responded - Indicate connection status per relay ### Mistake 27: Not Handling Large Threads **Problem**: Loading entire thread at once, performance issues. **Solution**: Implement pagination: ```javascript const loadThread = async (eventId, cursor = null) => { const filter = { "#e": [eventId], kinds: [1], limit: 20, until: cursor } const replies = await fetchEvents(filter) return { replies, nextCursor: replies[replies.length - 1]?.created_at } } ``` ## Testing ### Mistake 28: Not Testing with Multiple Relays **Problem**: App works with one relay but fails with others. **Solution**: Test with: - Fast relays - Slow relays - Unreliable relays - Paid relays (auth required) - Relays with different NIP support ### Mistake 29: Not Testing Edge Cases **Critical tests**: - Empty filter results - WebSocket disconnections - Malformed events - Very long content - Invalid signatures - Relay errors - Rate limiting - Concurrent operations ### Mistake 30: Not Monitoring Performance **Metrics to track**: - Event verification time - WebSocket latency per relay - Events per second processed - Memory usage (event cache) - Subscription count - Failed publishes ## Best Practices Checklist **Event Creation**: - [ ] Correct serialization for ID - [ ] Schnorr signatures - [ ] Current timestamp - [ ] Valid tag structure - [ ] Handle replaceable events **WebSocket**: - [ ] Handle EOSE - [ ] Close subscriptions - [ ] Process OK messages - [ ] Check WebSocket state - [ ] Reconnection logic **Filters**: - [ ] Set reasonable limits - [ ] Specific queries - [ ] Deduplicate arrays - [ ] Use prefix matching **Threading**: - [ ] Use root/reply markers - [ ] Include all p tags - [ ] Proper thread structure **Relays**: - [ ] Multiple relays - [ ] Implement NIP-65 - [ ] Respect limitations - [ ] Handle failures **Security**: - [ ] Never expose nsec - [ ] Verify all signatures - [ ] Use NIP-44 encryption - [ ] Sanitize content **UX**: - [ ] Cache events - [ ] Optimistic UI - [ ] Loading states - [ ] Pagination **Testing**: - [ ] Multiple relays - [ ] Edge cases - [ ] Monitor performance ## Resources - **nostr-tools**: JavaScript library with best practices - **rust-nostr**: Rust implementation with strong typing - **NIPs Repository**: Official specifications - **Nostr Dev**: Community resources and help