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.
This commit is contained in:
657
.claude/skills/nostr/references/common-mistakes.md
Normal file
657
.claude/skills/nostr/references/common-mistakes.md
Normal file
@@ -0,0 +1,657 @@
|
||||
# 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
|
||||
|
||||
361
.claude/skills/nostr/references/event-kinds.md
Normal file
361
.claude/skills/nostr/references/event-kinds.md
Normal file
@@ -0,0 +1,361 @@
|
||||
# Nostr Event Kinds - Complete Reference
|
||||
|
||||
This document provides a comprehensive list of all standard and commonly-used Nostr event kinds.
|
||||
|
||||
## Standard Event Kinds
|
||||
|
||||
### Core Events (0-999)
|
||||
|
||||
#### Metadata and Profile
|
||||
- **0**: `Metadata` - User profile information (name, about, picture, etc.)
|
||||
- Replaceable
|
||||
- Content: JSON with profile fields
|
||||
|
||||
#### Text Content
|
||||
- **1**: `Text Note` - Short-form post (like a tweet)
|
||||
- Regular event (not replaceable)
|
||||
- Most common event type
|
||||
|
||||
#### Relay Recommendations
|
||||
- **2**: `Recommend Relay` - Deprecated, use NIP-65 instead
|
||||
|
||||
#### Contact Lists
|
||||
- **3**: `Contacts` - Following list with optional relay hints
|
||||
- Replaceable
|
||||
- Tags: `p` tags for each followed user
|
||||
|
||||
#### Encrypted Messages
|
||||
- **4**: `Encrypted Direct Message` - Private message (NIP-04, deprecated)
|
||||
- Regular event
|
||||
- Use NIP-44 instead for better security
|
||||
|
||||
#### Content Management
|
||||
- **5**: `Event Deletion` - Request to delete events
|
||||
- Tags: `e` tags for events to delete
|
||||
- Only works for own events
|
||||
|
||||
#### Sharing
|
||||
- **6**: `Repost` - Share another event
|
||||
- Tags: `e` for reposted event, `p` for original author
|
||||
- May include original event in content
|
||||
|
||||
#### Reactions
|
||||
- **7**: `Reaction` - Like, emoji reaction to event
|
||||
- Content: "+" or emoji
|
||||
- Tags: `e` for reacted event, `p` for author
|
||||
|
||||
### Channel Events (40-49)
|
||||
|
||||
- **40**: `Channel Creation` - Create a public chat channel
|
||||
- **41**: `Channel Metadata` - Set channel name, about, picture
|
||||
- **42**: `Channel Message` - Post message in channel
|
||||
- **43**: `Channel Hide Message` - Hide a message in channel
|
||||
- **44**: `Channel Mute User` - Mute a user in channel
|
||||
|
||||
### Regular Events (1000-9999)
|
||||
|
||||
Regular events are never deleted or replaced. All versions are kept.
|
||||
|
||||
- **1000**: `Example regular event`
|
||||
- **1063**: `File Metadata` (NIP-94) - Metadata for shared files
|
||||
- Tags: url, MIME type, hash, size, dimensions
|
||||
|
||||
### Replaceable Events (10000-19999)
|
||||
|
||||
Only the latest event of each kind is kept per pubkey.
|
||||
|
||||
- **10000**: `Mute List` - List of muted users/content
|
||||
- **10001**: `Pin List` - Pinned events
|
||||
- **10002**: `Relay List Metadata` (NIP-65) - User's preferred relays
|
||||
- Critical for routing
|
||||
- Tags: `r` with relay URLs and read/write markers
|
||||
|
||||
### Ephemeral Events (20000-29999)
|
||||
|
||||
Not stored by relays, only forwarded once.
|
||||
|
||||
- **20000**: `Example ephemeral event`
|
||||
- **21000**: `Typing Indicator` - User is typing
|
||||
- **22242**: `Client Authentication` (NIP-42) - Auth response to relay
|
||||
|
||||
### Parameterized Replaceable Events (30000-39999)
|
||||
|
||||
Replaced based on `d` tag value.
|
||||
|
||||
#### Lists (30000-30009)
|
||||
- **30000**: `Categorized People List` - Custom people lists
|
||||
- `d` tag: list identifier
|
||||
- `p` tags: people in list
|
||||
|
||||
- **30001**: `Categorized Bookmark List` - Bookmark collections
|
||||
- `d` tag: list identifier
|
||||
- `e` or `a` tags: bookmarked items
|
||||
|
||||
- **30008**: `Badge Definition` (NIP-58) - Define a badge/achievement
|
||||
- `d` tag: badge ID
|
||||
- Tags: name, description, image
|
||||
|
||||
- **30009**: `Profile Badges` (NIP-58) - Badges displayed on profile
|
||||
- `d` tag: badge ID
|
||||
- `e` or `a` tags: badge awards
|
||||
|
||||
#### Long-form Content (30023)
|
||||
- **30023**: `Long-form Article` (NIP-23) - Blog post, article
|
||||
- `d` tag: article identifier (slug)
|
||||
- Tags: title, summary, published_at, image
|
||||
- Content: Markdown
|
||||
|
||||
#### Application Data (30078)
|
||||
- **30078**: `Application-specific Data` (NIP-78)
|
||||
- `d` tag: app-name:data-key
|
||||
- Content: app-specific data (may be encrypted)
|
||||
|
||||
#### Other Parameterized Replaceables
|
||||
- **31989**: `Application Handler Information` (NIP-89)
|
||||
- Declares app can handle certain event kinds
|
||||
|
||||
- **31990**: `Handler Recommendation` (NIP-89)
|
||||
- User's preferred apps for event kinds
|
||||
|
||||
## Special Event Kinds
|
||||
|
||||
### Authentication & Signing
|
||||
- **22242**: `Client Authentication` - Prove key ownership to relay
|
||||
- **24133**: `Nostr Connect` - Remote signer protocol (NIP-46)
|
||||
|
||||
### Lightning & Payments
|
||||
- **9734**: `Zap Request` (NIP-57) - Request Lightning payment
|
||||
- Not published to regular relays
|
||||
- Sent to LNURL provider
|
||||
|
||||
- **9735**: `Zap Receipt` (NIP-57) - Proof of Lightning payment
|
||||
- Published by LNURL provider
|
||||
- Proves zap was paid
|
||||
|
||||
- **23194**: `Wallet Request` (NIP-47) - Request wallet operation
|
||||
- **23195**: `Wallet Response` (NIP-47) - Response to wallet request
|
||||
|
||||
### Content & Annotations
|
||||
- **1984**: `Reporting` (NIP-56) - Report content/users
|
||||
- Tags: reason (spam, illegal, etc.)
|
||||
|
||||
- **9802**: `Highlights` (NIP-84) - Highlight text
|
||||
- Content: highlighted text
|
||||
- Tags: context, source event
|
||||
|
||||
### Badges & Reputation
|
||||
- **8**: `Badge Award` (NIP-58) - Award a badge to someone
|
||||
- Tags: `a` for badge definition, `p` for recipient
|
||||
|
||||
### Generic Events
|
||||
- **16**: `Generic Repost` (NIP-18) - Repost any event kind
|
||||
- More flexible than kind 6
|
||||
|
||||
- **27235**: `HTTP Auth` (NIP-98) - Authenticate HTTP requests
|
||||
- Tags: URL, method
|
||||
|
||||
## Event Kind Ranges Summary
|
||||
|
||||
| Range | Type | Behavior | Examples |
|
||||
|-------|------|----------|----------|
|
||||
| 0-999 | Core | Varies | Metadata, notes, reactions |
|
||||
| 1000-9999 | Regular | Immutable, all kept | File metadata |
|
||||
| 10000-19999 | Replaceable | Only latest kept | Mute list, relay list |
|
||||
| 20000-29999 | Ephemeral | Not stored | Typing, presence |
|
||||
| 30000-39999 | Parameterized Replaceable | Replaced by `d` tag | Articles, lists, badges |
|
||||
|
||||
## Event Lifecycle
|
||||
|
||||
### Regular Events (1000-9999)
|
||||
```
|
||||
Event A published → Stored
|
||||
Event A' published → Both A and A' stored
|
||||
```
|
||||
|
||||
### Replaceable Events (10000-19999)
|
||||
```
|
||||
Event A published → Stored
|
||||
Event A' published (same kind, same pubkey) → A deleted, A' stored
|
||||
```
|
||||
|
||||
### Parameterized Replaceable Events (30000-39999)
|
||||
```
|
||||
Event A (d="foo") published → Stored
|
||||
Event B (d="bar") published → Both stored (different d)
|
||||
Event A' (d="foo") published → A deleted, A' stored (same d)
|
||||
```
|
||||
|
||||
### Ephemeral Events (20000-29999)
|
||||
```
|
||||
Event A published → Forwarded to subscribers, NOT stored
|
||||
```
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### Metadata (Kind 0)
|
||||
```json
|
||||
{
|
||||
"kind": 0,
|
||||
"content": "{\"name\":\"Alice\",\"about\":\"Nostr user\",\"picture\":\"https://...\",\"nip05\":\"alice@example.com\"}",
|
||||
"tags": []
|
||||
}
|
||||
```
|
||||
|
||||
### Text Note (Kind 1)
|
||||
```json
|
||||
{
|
||||
"kind": 1,
|
||||
"content": "Hello Nostr!",
|
||||
"tags": [
|
||||
["t", "nostr"],
|
||||
["t", "hello"]
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Reply (Kind 1 with thread tags)
|
||||
```json
|
||||
{
|
||||
"kind": 1,
|
||||
"content": "Great post!",
|
||||
"tags": [
|
||||
["e", "<root-event-id>", "<relay>", "root"],
|
||||
["e", "<parent-event-id>", "<relay>", "reply"],
|
||||
["p", "<author-pubkey>"]
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Reaction (Kind 7)
|
||||
```json
|
||||
{
|
||||
"kind": 7,
|
||||
"content": "+",
|
||||
"tags": [
|
||||
["e", "<reacted-event-id>"],
|
||||
["p", "<event-author-pubkey>"],
|
||||
["k", "1"]
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Long-form Article (Kind 30023)
|
||||
```json
|
||||
{
|
||||
"kind": 30023,
|
||||
"content": "# My Article\n\nContent here...",
|
||||
"tags": [
|
||||
["d", "my-article-slug"],
|
||||
["title", "My Article"],
|
||||
["summary", "This is about..."],
|
||||
["published_at", "1234567890"],
|
||||
["t", "nostr"],
|
||||
["image", "https://..."]
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Relay List (Kind 10002)
|
||||
```json
|
||||
{
|
||||
"kind": 10002,
|
||||
"content": "",
|
||||
"tags": [
|
||||
["r", "wss://relay1.com"],
|
||||
["r", "wss://relay2.com", "write"],
|
||||
["r", "wss://relay3.com", "read"]
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Zap Request (Kind 9734)
|
||||
```json
|
||||
{
|
||||
"kind": 9734,
|
||||
"content": "",
|
||||
"tags": [
|
||||
["relays", "wss://relay1.com", "wss://relay2.com"],
|
||||
["amount", "21000"],
|
||||
["lnurl", "lnurl..."],
|
||||
["p", "<recipient-pubkey>"],
|
||||
["e", "<event-id>"]
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### File Metadata (Kind 1063)
|
||||
```json
|
||||
{
|
||||
"kind": 1063,
|
||||
"content": "My photo from the trip",
|
||||
"tags": [
|
||||
["url", "https://cdn.example.com/image.jpg"],
|
||||
["m", "image/jpeg"],
|
||||
["x", "abc123..."],
|
||||
["size", "524288"],
|
||||
["dim", "1920x1080"],
|
||||
["blurhash", "LEHV6n..."]
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Report (Kind 1984)
|
||||
```json
|
||||
{
|
||||
"kind": 1984,
|
||||
"content": "This is spam",
|
||||
"tags": [
|
||||
["e", "<reported-event-id>", "<relay>"],
|
||||
["p", "<reported-pubkey>"],
|
||||
["report", "spam"]
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Future Event Kinds
|
||||
|
||||
The event kind space is open-ended. New NIPs may define new event kinds.
|
||||
|
||||
**Guidelines for new event kinds**:
|
||||
1. Use appropriate range for desired behavior
|
||||
2. Document in a NIP
|
||||
3. Implement in at least 2 clients and 1 relay
|
||||
4. Ensure backwards compatibility
|
||||
5. Don't overlap with existing kinds
|
||||
|
||||
**Custom event kinds**:
|
||||
- Applications can use undefined event kinds
|
||||
- Document behavior for interoperability
|
||||
- Consider proposing as a NIP if useful broadly
|
||||
|
||||
## Event Kind Selection Guide
|
||||
|
||||
**Choose based on lifecycle needs**:
|
||||
|
||||
- **Regular (1000-9999)**: When you need history
|
||||
- User posts, comments, reactions
|
||||
- Payment records, receipts
|
||||
- Immutable records
|
||||
|
||||
- **Replaceable (10000-19999)**: When you need latest state
|
||||
- User settings, preferences
|
||||
- Mute/block lists
|
||||
- Current status
|
||||
|
||||
- **Ephemeral (20000-29999)**: When you need real-time only
|
||||
- Typing indicators
|
||||
- Online presence
|
||||
- Temporary notifications
|
||||
|
||||
- **Parameterized Replaceable (30000-39999)**: When you need multiple latest states
|
||||
- Articles (one per slug)
|
||||
- Product listings (one per product ID)
|
||||
- Configuration sets (one per setting name)
|
||||
|
||||
## References
|
||||
|
||||
- NIPs Repository: https://github.com/nostr-protocol/nips
|
||||
- NIP-16: Event Treatment
|
||||
- NIP-01: Event structure
|
||||
- Various feature NIPs for specific kinds
|
||||
|
||||
1170
.claude/skills/nostr/references/nips-overview.md
Normal file
1170
.claude/skills/nostr/references/nips-overview.md
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user