# 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
  <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**:
```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": ["<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**:
```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-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

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

