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.
Unlike simple allow/deny lists, curation mode classifies publishers into three tiers:
| Tier | Rate Limited | Daily Limit | Visibility |
|---|---|---|---|
| Trusted | No | Unlimited | Full |
| Blacklisted | N/A (blocked) | 0 | Hidden from regular users |
| Unclassified | Yes | 50 events/day (default) | Full |
This allows relay operators to:
export ORLY_ACL_MODE=curating
export ORLY_OWNERS=npub1your_owner_pubkey
./orly
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\"]}"
}
Use the web UI or NIP-86 API to:
| Variable | Default | Description |
|---|---|---|
ORLY_ACL_MODE | none | Set to curating to enable |
ORLY_OWNERS | Owner pubkeys (can configure relay) | |
ORLY_ADMINS | Admin pubkeys (can manage publishers) |
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"]
}
Pre-defined categories for convenient kind whitelisting:
| Category | Kinds | Description |
|---|---|---|
social | 0, 1, 3, 6, 7, 10002 | Profiles, notes, contacts, reposts, reactions |
dm | 4, 14, 1059 | Direct messages (NIP-04, NIP-17, gift wraps) |
longform | 30023, 30024 | Long-form articles and drafts |
media | 1063, 20, 21, 22 | File metadata, picture/video/audio events |
marketplace | 30017-30020, 1021, 1022 | Products, stalls, auctions, bids |
groups_nip29 | 9-12, 9000-9002, 39000-39002 | NIP-29 relay-based groups |
groups_nip72 | 34550, 1111, 4550 | NIP-72 moderated communities |
lists | 10000, 10001, 10003, 30000, 30001, 30003 | Mute, pin, bookmark lists |
Example configuration allowing social interactions and DMs:
{
"kindCategories": ["social", "dm"],
"dailyLimit": 100,
"ipDailyLimit": 1000
}
Trusted publishers have unlimited publishing rights:
Use case: Known quality contributors, verified community members, partner relays.
Blacklisted publishers are blocked from publishing:
"pubkey is blacklisted" errorUse case: Spammers, abusive users, bad actors.
Everyone else falls into the unclassified tier:
Use case: New users, general public.
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:
"daily event limit exceeded" errorTo 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:
When rate limits are exceeded:
| Offense | Ban Duration | Description |
|---|---|---|
| First | 1 hour | Quick timeout for accidental over-posting |
| Second+ | 1 week | Extended ban for repeated abuse |
Ban durations are configurable via firstBanHours and secondBanHours.
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.
Events can be flagged as spam without deletion:
This is useful for:
All management operations use NIP-98 HTTP authentication.
# 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 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":[]}'
# 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.
# 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":[]}'
# 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>"]}'
# 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":[]}'
The curation web UI is available at /#curation and provides:
Curation data is stored in the relay database with the following key prefixes:
| Prefix | Purpose |
|---|---|
CURATING_ACL_CONFIG | Current 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 |
For performance, the following data is cached in memory:
Caches are refreshed every hour by the background cleanup goroutine.
A background goroutine runs hourly to:
`json
{"dailyLimit": 100, "ipDailyLimit": 1000, "kindCategories": ["social"]}
`
During spam attacks:
ipDailyLimitunblacklistpubkey - their events become visible againunmarkspam - event becomes visible againunblockip - IP can publish again immediately| Feature | None | Follows | Managed | Curating |
|---|---|---|---|---|
| Default Access | Write | Write if followed | Explicit allow | Rate-limited |
| Rate Limiting | No | No | No | Yes |
| Kind Filtering | No | No | Optional | Yes |
| IP Protection | No | No | No | Yes |
| Spam Flagging | No | No | No | Yes |
| Configuration | Env vars | Follow lists | NIP-86 | Kind 30078 events |
| Web UI | Basic | Basic | Basic | Full curation panel |
The relay requires a configuration event before accepting any events. Publish a kind 30078 event with d-tag curating-config.
The user has exceeded their daily limit. Options:
dailyLimit in configurationThe pubkey is on the blacklist. Use unblacklistpubkey if this was a mistake.
The IP has been auto-banned due to rate limit violations. Use unblockip if legitimate, or wait for the ban to expire.
Check if the event author has been blacklisted. Blacklisted authors' events are hidden from regular users but visible to admins.