CURATION_MODE_GUIDE.md raw

Curation Mode Guide

Curation mode is a sophisticated access control system for Nostr relays that provides three-tier publisher classification, rate limiting, IP-based flood protection, and event kind whitelisting.

Overview

Unlike simple allow/deny lists, curation mode classifies publishers into three tiers:

TierRate LimitedDaily LimitVisibility
TrustedNoUnlimitedFull
BlacklistedN/A (blocked)0Hidden from regular users
UnclassifiedYes50 events/day (default)Full

This allows relay operators to:

Quick Start

1. Start the Relay

export ORLY_ACL_MODE=curating
export ORLY_OWNERS=npub1your_owner_pubkey
./orly

2. Publish Configuration

The relay will not accept events until you publish a configuration event. Use the web UI at http://your-relay/#curation or publish a kind 30078 event:

{
  "kind": 30078,
  "tags": [["d", "curating-config"]],
  "content": "{\"dailyLimit\":50,\"ipDailyLimit\":500,\"firstBanHours\":1,\"secondBanHours\":168,\"kindCategories\":[\"social\"]}"
}

3. Manage Publishers

Use the web UI or NIP-86 API to:

Configuration

Environment Variables

VariableDefaultDescription
ORLY_ACL_MODEnoneSet to curating to enable
ORLY_OWNERSOwner pubkeys (can configure relay)
ORLY_ADMINSAdmin pubkeys (can manage publishers)

Configuration Event (Kind 30078)

Configuration is stored as a replaceable Nostr event (kind 30078) with d-tag curating-config. Only owners and admins can publish configuration.

interface CuratingConfig {
  // Rate Limiting
  dailyLimit: number;      // Max events/day for unclassified users (default: 50)
  ipDailyLimit: number;    // Max events/day from single IP (default: 500)

  // IP Ban Durations
  firstBanHours: number;   // First offense ban duration (default: 1 hour)
  secondBanHours: number;  // Subsequent offense ban duration (default: 168 hours / 1 week)

  // Kind Filtering (choose one or combine)
  allowedKinds: number[];     // Explicit kind numbers: [0, 1, 3, 7]
  allowedRanges: string[];    // Kind ranges: ["1000-1999", "30000-39999"]
  kindCategories: string[];   // Pre-defined categories: ["social", "dm"]
}

Event Kind Categories

Pre-defined categories for convenient kind whitelisting:

CategoryKindsDescription
social0, 1, 3, 6, 7, 10002Profiles, notes, contacts, reposts, reactions
dm4, 14, 1059Direct messages (NIP-04, NIP-17, gift wraps)
longform30023, 30024Long-form articles and drafts
media1063, 20, 21, 22File metadata, picture/video/audio events
marketplace30017-30020, 1021, 1022Products, stalls, auctions, bids
groups_nip299-12, 9000-9002, 39000-39002NIP-29 relay-based groups
groups_nip7234550, 1111, 4550NIP-72 moderated communities
lists10000, 10001, 10003, 30000, 30001, 30003Mute, pin, bookmark lists

Example configuration allowing social interactions and DMs:

{
  "kindCategories": ["social", "dm"],
  "dailyLimit": 100,
  "ipDailyLimit": 1000
}

Three-Tier Classification

Trusted Publishers

Trusted publishers have unlimited publishing rights:

Use case: Known quality contributors, verified community members, partner relays.

Blacklisted Publishers

Blacklisted publishers are blocked from publishing:

Use case: Spammers, abusive users, bad actors.

Unclassified Publishers

Everyone else falls into the unclassified tier:

Use case: New users, general public.

Rate Limiting & Flood Protection

Per-Pubkey Limits

Unclassified publishers are limited to a configurable number of events per day (default: 50). The count resets at midnight UTC.

When a user exceeds their limit:

  1. Event is rejected with "daily event limit exceeded" error
  2. Their IP is flagged for potential abuse

Per-IP Limits

To prevent Sybil attacks (creating many pubkeys from one IP), there's also an IP-based daily limit (default: 500 events).

When an IP exceeds its limit:

  1. All events from that IP are rejected
  2. The IP is temporarily banned

Automatic IP Banning

When rate limits are exceeded:

OffenseBan DurationDescription
First1 hourQuick timeout for accidental over-posting
Second+1 weekExtended ban for repeated abuse

Ban durations are configurable via firstBanHours and secondBanHours.

Offense Tracking

The system tracks which pubkeys triggered rate limits from each IP:

IP 192.168.1.100:
  - npub1abc... exceeded limit at 2024-01-15 10:30:00
  - npub1xyz... exceeded limit at 2024-01-15 10:45:00
  Offense count: 2
  Status: Banned until 2024-01-22 10:45:00

This helps identify coordinated spam attacks.

Spam Flagging

Events can be flagged as spam without deletion:

This is useful for:

NIP-86 Management API

All management operations use NIP-98 HTTP authentication.

Trust Management

# Trust a pubkey
curl -X POST https://relay.example.com \
  -H "Authorization: Nostr <nip98_token>" \
  -d '{"method":"trustpubkey","params":["<pubkey_hex>"]}'

# Untrust a pubkey
curl -X POST https://relay.example.com \
  -H "Authorization: Nostr <nip98_token>" \
  -d '{"method":"untrustpubkey","params":["<pubkey_hex>"]}'

# List trusted pubkeys
curl -X POST https://relay.example.com \
  -H "Authorization: Nostr <nip98_token>" \
  -d '{"method":"listtrustedpubkeys","params":[]}'

Blacklist Management

# Blacklist a pubkey
curl -X POST https://relay.example.com \
  -H "Authorization: Nostr <nip98_token>" \
  -d '{"method":"blacklistpubkey","params":["<pubkey_hex>"]}'

# Remove from blacklist
curl -X POST https://relay.example.com \
  -H "Authorization: Nostr <nip98_token>" \
  -d '{"method":"unblacklistpubkey","params":["<pubkey_hex>"]}'

# List blacklisted pubkeys
curl -X POST https://relay.example.com \
  -H "Authorization: Nostr <nip98_token>" \
  -d '{"method":"listblacklistedpubkeys","params":[]}'

Unclassified User Management

# List unclassified users sorted by event count
curl -X POST https://relay.example.com \
  -H "Authorization: Nostr <nip98_token>" \
  -d '{"method":"listunclassifiedusers","params":[]}'

Response includes pubkey, event count, and last activity for each user.

Spam Management

# Mark event as spam
curl -X POST https://relay.example.com \
  -H "Authorization: Nostr <nip98_token>" \
  -d '{"method":"markspam","params":["<event_id_hex>"]}'

# Unmark spam
curl -X POST https://relay.example.com \
  -H "Authorization: Nostr <nip98_token>" \
  -d '{"method":"unmarkspam","params":["<event_id_hex>"]}'

# List spam events
curl -X POST https://relay.example.com \
  -H "Authorization: Nostr <nip98_token>" \
  -d '{"method":"listspamevents","params":[]}'

IP Block Management

# List blocked IPs
curl -X POST https://relay.example.com \
  -H "Authorization: Nostr <nip98_token>" \
  -d '{"method":"listblockedips","params":[]}'

# Unblock an IP
curl -X POST https://relay.example.com \
  -H "Authorization: Nostr <nip98_token>" \
  -d '{"method":"unblockip","params":["<ip_address>"]}'

Configuration Management

# Get current configuration
curl -X POST https://relay.example.com \
  -H "Authorization: Nostr <nip98_token>" \
  -d '{"method":"getcuratingconfig","params":[]}'

# Set allowed kind categories
curl -X POST https://relay.example.com \
  -H "Authorization: Nostr <nip98_token>" \
  -d '{"method":"setallowedkindcategories","params":[["social","dm","longform"]]}'

# Get allowed kind categories
curl -X POST https://relay.example.com \
  -H "Authorization: Nostr <nip98_token>" \
  -d '{"method":"getallowedkindcategories","params":[]}'

Web UI

The curation web UI is available at /#curation and provides:

Database Storage

Curation data is stored in the relay database with the following key prefixes:

PrefixPurpose
CURATING_ACL_CONFIGCurrent configuration
CURATING_ACL_TRUSTED_PUBKEY_{pubkey}Trusted publisher list
CURATING_ACL_BLACKLISTED_PUBKEY_{pubkey}Blacklisted publisher list
CURATING_ACL_EVENT_COUNT_{pubkey}_{date}Daily event counts per pubkey
CURATING_ACL_IP_EVENT_COUNT_{ip}_{date}Daily event counts per IP
CURATING_ACL_IP_OFFENSE_{ip}Offense tracking per IP
CURATING_ACL_BLOCKED_IP_{ip}Active IP blocks
CURATING_ACL_SPAM_EVENT_{eventID}Spam-flagged events

Caching

For performance, the following data is cached in memory:

Caches are refreshed every hour by the background cleanup goroutine.

Background Maintenance

A background goroutine runs hourly to:

  1. Remove expired IP blocks
  2. Clean up old event count entries (older than 2 days)
  3. Refresh in-memory caches
  4. Log maintenance statistics

Best Practices

Starting a New Curated Relay

  1. Start with permissive settings:

`json {"dailyLimit": 100, "ipDailyLimit": 1000, "kindCategories": ["social"]} `

  1. Monitor unclassified users for a few days
  1. Trust active, quality contributors
  1. Blacklist obvious spammers
  1. Adjust rate limits based on observed patterns

Handling Spam Waves

During spam attacks:

  1. The IP-based flood protection will auto-ban attack sources
  2. Review blocked IPs via web UI or API
  3. Blacklist any pubkeys that got through
  4. Consider temporarily lowering ipDailyLimit

Recovering from Mistakes

Comparison with Other ACL Modes

FeatureNoneFollowsManagedCurating
Default AccessWriteWrite if followedExplicit allowRate-limited
Rate LimitingNoNoNoYes
Kind FilteringNoNoOptionalYes
IP ProtectionNoNoNoYes
Spam FlaggingNoNoNoYes
ConfigurationEnv varsFollow listsNIP-86Kind 30078 events
Web UIBasicBasicBasicFull curation panel

Troubleshooting

"Relay not accepting events"

The relay requires a configuration event before accepting any events. Publish a kind 30078 event with d-tag curating-config.

"daily event limit exceeded"

The user has exceeded their daily limit. Options:

  1. Wait until midnight UTC for reset
  2. Trust the pubkey if they're a quality contributor
  3. Increase dailyLimit in configuration

"pubkey is blacklisted"

The pubkey is on the blacklist. Use unblacklistpubkey if this was a mistake.

"IP is blocked"

The IP has been auto-banned due to rate limit violations. Use unblockip if legitimate, or wait for the ban to expire.

Events disappearing for users

Check if the event author has been blacklisted. Blacklisted authors' events are hidden from regular users but visible to admins.