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:
2025-11-04 12:54:41 +00:00
parent f1c636db41
commit effb3fafc1
5 changed files with 2799 additions and 0 deletions

View 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

View 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

File diff suppressed because it is too large Load Diff