Files
next.orly.dev/.claude/skills/nostr/references/common-mistakes.md
mleku effb3fafc1 Add Nostr Protocol Skill and Reference Materials
- Introduced a comprehensive skill for the Nostr protocol, covering client and relay implementation, event structure, cryptographic operations, and best practices.
- Added detailed reference files including an overview of NIPs, event kinds, common mistakes, and a complete guide to the Nostr protocol.
- Included quick start examples and a checklist for implementing Nostr features effectively.
- Ensured documentation is aligned with the latest standards and practices for Nostr development.
2025-11-04 12:54:41 +00:00

14 KiB

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:

[
  0,                    // Must be integer 0
  <pubkey>,            // Lowercase hex string
  <created_at>,        // Unix timestamp integer
  <kind>,              // Integer
  <tags>,              // Array of arrays
  <content>            // 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:

{
  "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:

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

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:

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:

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:

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:

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:

{
  "kinds": [1],
  "limit": 10000
}

Good:

{
  "kinds": [1],
  "authors": ["<followed-users>"],
  "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:

{
  "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:

{
  "authors": ["pubkey1", "pubkey1"],
  "kinds": [1, 1]
}

Good:

{
  "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):

{
  "kind": 1,
  "tags": [
    ["e", "<root-event-id>", "<relay>", "root"],
    ["e", "<parent-event-id>", "<relay>", "reply"],
    ["p", "<author1-pubkey>"],
    ["p", "<author2-pubkey>"]
  ]
}

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
{
  "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):

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
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:

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:

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:

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:

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:

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:

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