A Python-based moderation system for Nostr relays running strfry, specifically designed for thelookup content moderation.
This system monitors wot.nostr.net (a Web of Trust filtered relay) for kind 1984 moderation reports, tracks them in a database, and automatically deletes heavily reported content from your strfry relay.
wot.nostr.net → Monitor Daemon → SQLite DB → strfry delete
↓ ↓
(kind 1984) Track reports Delete reported events
(WoT filtered) Count threshold (when threshold met)
- Monitor: Connects to wot.nostr.net and listens for kind 1984 (reporting) events
- Track: Stores reports in SQLite database with timestamps and report types
- Delete: When report threshold is met, uses
strfry deleteCLI to remove content - Publish (optional): Publishes kind 5 delete events to relays
Why wot.nostr.net? It already filters events by Web of Trust, so you only see reports from trusted users in your network. No need to build WoT yourself!
- Simple: Single daemon, no plugins needed
- WoT-filtered: Only processes reports from wot.nostr.net (pre-filtered by your trust network)
- Configurable thresholds: Different limits per report type (illegal=1, spam=5, etc.)
- Time-windowed: Only count recent reports (default 30 days)
- Auto-delete: Automatically removes content using strfry CLI
- Delete events: Optionally publishes kind 5 delete events
- SQLite database: Persistent report tracking
- Systemd ready: Easy deployment as a service
- Python 3.8+
- strfry relay (only needed for actual deletion, not for dry run)
Using pip:
# Clone repository
git clone https://github.com/nostr-net/lookup-moderator.git
cd lookup-moderator
# Install dependencies
pip install -r requirements.txt
# Configure
cp config.yaml.example config.yaml
nano config.yaml # Edit configuration
# Run
python3 lookup_moderator.pyUsing uv (recommended for faster installs):
# Clone repository
git clone https://github.com/nostr-net/lookup-moderator.git
cd lookup-moderator
# Install uv if you haven't already
curl -LsSf https://astral.sh/uv/install.sh | sh
# Install dependencies
uv pip install -r requirements.txt
# Or use uv to run directly
uv run lookup_moderator.py
# Configure
cp config.yaml.example config.yaml
nano config.yaml # Edit configurationWant to see what the tool finds without actually deleting anything? Use dry run mode!
Dry run mode lets you:
- Monitor what reports are coming in from wot.nostr.net
- See which events would be deleted based on your thresholds
- Test your configuration safely without making any changes
- Understand what content is being reported in your network
-
Copy the example config:
cp config.yaml.example config.yaml
-
Enable dry run mode in config.yaml:
moderation: dry_run: true # Enable dry run mode auto_delete: true # Can be true or false, no deletion happens in dry run
-
Run the tool:
# Using Python python3 lookup_moderator.py # Or using uv uv run lookup_moderator.py
Example 1: Monitor everything, see what would be deleted
moderation:
report_threshold: 3
time_window_days: 30
auto_delete: true
dry_run: true # Nothing will actually be deleted
strfry:
executable: "/usr/local/bin/strfry" # Path doesn't need to exist in dry run
data_dir: "/var/lib/strfry" # Path doesn't need to exist in dry run
publish_deletes: true # Would publish, but won't in dry run modeExample 2: Test strict illegal content moderation
moderation:
report_threshold: 3
time_window_days: 7 # Shorter window for testing
type_thresholds:
illegal: 1 # See what gets flagged immediately
malware: 1
spam: 5
auto_delete: true
dry_run: true # Safe to test strict settingsExample 3: Monitor without auto-delete intent
moderation:
report_threshold: 3
auto_delete: false # Just monitor, don't even simulate deletion
dry_run: true # Extra safetyWhen a report threshold is reached, you'll see output like:
================================================================================
NEW MODERATION REPORT
Report ID: abc123def456...
Reporter: 789pubkey012...
Reported Event: xyz789event...
Report Type: spam
Content: This is spam content reported by user
Total reports: 3 (threshold: 3)
THRESHOLD REACHED - Event should be deleted!
[DRY RUN MODE] Simulating deletion process...
Auto-delete enabled, deleting event...
[DRY RUN] Would execute: /usr/local/bin/strfry delete --dir /var/lib/strfry --id xyz789event...
[DRY RUN] Would delete event xyz789event... from strfry
[DRY RUN] Would publish kind 5 delete event for xyz789event...
[DRY RUN] Would publish to relays: ['wss://wot.nostr.net']
[DRY RUN] Reason: Reported 3 times: spam
Event xyz789event... deleted successfully
================================================================================
Notice all the [DRY RUN] prefixes - no actual commands are executed!
Once you're satisfied with what you see:
-
Update config.yaml:
moderation: dry_run: false # Disable dry run mode
-
Verify strfry paths are correct:
which strfry ls -la /var/lib/strfry/strfry.conf
-
Test strfry delete manually:
/usr/local/bin/strfry delete --help
-
Restart the moderator:
python3 lookup_moderator.py # Or with systemd sudo systemctl restart lookup-moderator
Edit config.yaml:
# WoT Relay (wot.nostr.net already filters by your WoT)
wot_relay:
url: "wss://wot.nostr.net"
# Optional: For publishing delete events
pubkey: "your_pubkey_hex"
private_key: "nsec_or_hex" # Keep secure!
# Moderation settings
moderation:
report_threshold: 3 # Default reports needed
time_window_days: 30 # Only count recent reports
auto_delete: true # Auto-delete when threshold met
# Type-specific thresholds
type_thresholds:
illegal: 1 # Immediate removal
malware: 1
spam: 5 # More tolerance
impersonation: 2
# Strfry configuration
strfry:
executable: "/usr/local/bin/strfry"
data_dir: "/var/lib/strfry" # Directory with strfry.conf
# Publish kind 5 delete events
publish_deletes: true
publish_relays:
- "wss://wot.nostr.net"
# Event kinds to monitor (thelookup-specific)
events:
monitored_kinds:
- 30817 # Custom NIPs
- 31990 # Application directory- wot.nostr.net: The relay already filters by your Web of Trust, so you only see reports from trusted users
- private_key: Only needed if you want to publish kind 5 delete events (optional)
- strfry paths: Adjust
executableanddata_dirto match your installation
Using Python:
python3 lookup_moderator.py
# With custom config
python3 lookup_moderator.py --config /path/to/config.yamlUsing uv:
uv run lookup_moderator.py
# With custom config
uv run lookup_moderator.py --config /path/to/config.yaml
# Or create a virtual environment and run
uv venv
source .venv/bin/activate # On Windows: .venv\Scripts\activate
uv pip install -r requirements.txt
python lookup_moderator.pyUsing Python:
Create /etc/systemd/system/lookup-moderator.service:
[Unit]
Description=Lookup Moderator - Nostr Moderation Monitor
After=network.target
[Service]
Type=simple
User=strfry
WorkingDirectory=/path/to/lookup-moderator
ExecStart=/usr/bin/python3 /path/to/lookup-moderator/lookup_moderator.py
Restart=always
RestartSec=10
[Install]
WantedBy=multi-user.targetUsing uv:
Create /etc/systemd/system/lookup-moderator.service:
[Unit]
Description=Lookup Moderator - Nostr Moderation Monitor
After=network.target
[Service]
Type=simple
User=strfry
WorkingDirectory=/path/to/lookup-moderator
ExecStart=/home/user/.cargo/bin/uv run /path/to/lookup-moderator/lookup_moderator.py
Restart=always
RestartSec=10
Environment="PATH=/home/user/.cargo/bin:/usr/local/bin:/usr/bin"
[Install]
WantedBy=multi-user.targetEnable and start:
sudo systemctl enable lookup-moderator
sudo systemctl start lookup-moderator
sudo systemctl status lookup-moderator# Real-time logs
tail -f moderator.log
# Systemd logs
sudo journalctl -u lookup-moderator -fWhen a report threshold is reached:
-
Strfry CLI Delete: Executes
strfry delete --dir /path --id <event_id>- Removes event from local strfry database
- Immediate local deletion
-
Publish Delete Event (optional): Creates and publishes kind 5 event
- Notifies other relays to delete the content
- Per NIP-09
- Requires private key in config
Per NIP-56:
nudity- Explicit contentmalware- Malicious softwareprofanity- Hateful/offensive speechillegal- Potentially illegal contentspam- Unsolicited messagesimpersonation- False identityother- Other issues
Set different thresholds per type in config.
SQLite database stores:
- All kind 1984 reports
- Reporter pubkeys
- Report types and timestamps
- Reported event IDs
Location: ./moderation_reports.db (configurable)
Auto-cleanup: Old reports are automatically removed based on time_window_days × 2
# View all reports
sqlite3 moderation_reports.db "SELECT * FROM reports;"
# Count reports per event
sqlite3 moderation_reports.db "
SELECT reported_event_id, COUNT(*) as count
FROM reports
GROUP BY reported_event_id
ORDER BY count DESC;
"
# Reports by type
sqlite3 moderation_reports.db "
SELECT report_type, COUNT(*) as count
FROM reports
GROUP BY report_type;
"================================================================================
Lookup Moderator - Nostr Kind 1984 Event Monitor
================================================================================
Database stats:
Total reports: 47
Unique reported events: 12
Unique reporters: 23
Configuration:
WoT Relay: wss://wot.nostr.net
Report threshold: 3
Time window: 30 days
Auto-delete: True
Monitored kinds: [30817, 31990]
Connecting to wss://wot.nostr.net...
Added relay: wss://wot.nostr.net
Connected to relay!
Subscribing to kind 1984 moderation events...
Monitoring started. Press Ctrl+C to stop.
================================================================================
NEW MODERATION REPORT
Report ID: abc123def456...
Reporter: 789pubkey012...
Reported Event: xyz789event...
Report Type: spam
Content: This is spam content reported by user
Total reports: 3 (threshold: 3)
THRESHOLD REACHED - Event should be deleted!
Auto-delete enabled, deleting event...
Executing: /usr/local/bin/strfry delete --dir /var/lib/strfry --id xyz789event...
Successfully deleted event xyz789event... from strfry
Published delete event for xyz789event... to 1 relays
Event xyz789event... deleted successfully
================================================================================
Check paths:
which strfry # Should match config.yaml executable path
ls -la /var/lib/strfry # Should exist and contain strfry.confCheck permissions:
# Make sure the user running the script can execute strfry
sudo -u strfry /usr/local/bin/strfry delete --helpTest manually:
# Try deleting an event manually
/usr/local/bin/strfry delete --dir /var/lib/strfry --id <some_event_id>- Make sure wot.nostr.net is accessible:
curl -I https://wot.nostr.net - Check you're in someone's Web of Trust (wot.nostr.net filters by WoT)
- Verify monitored_kinds includes the events being reported
- Check logs for connection errors
- Make sure
private_keyis set in config - Verify
publish_deletes: true - Check
publish_relayslist is not empty - Look for errors in logs about key parsing
If publishing delete events:
- Store
config.yamlwith restricted permissions:chmod 600 config.yaml - Consider using environment variables for private key
- Or use a dedicated moderation keypair (not your main key)
- WoT filtering: wot.nostr.net only shows reports from your trust network
- Threshold-based: Multiple reports required (configurable)
- Time-bounded: Old reports expire
- Transparent: All reports in local database
- Report spam: Mitigated by WoT filtering (only trusted reporters)
- False reports: Mitigated by threshold requirements
- Stale reports: Mitigated by time windows
- Mass deletion: Set appropriate thresholds per type
- CPU: Low (event-driven)
- Memory: ~50-100 MB
- Network: Minimal (single relay connection)
- Disk: Database grows ~1 KB per report
- Latency: Delete executes within seconds of threshold
lookup-moderator/
├── lookup_moderator.py # Main monitoring daemon
├── moderation_db.py # SQLite database abstraction
├── config.yaml.example # Configuration template
├── requirements.txt # Python dependencies (pip)
├── pyproject.toml # Project metadata (uv/pip)
└── README.md # This file
Test with Python:
# Test database
python3 -c "from moderation_db import ModerationDB; db = ModerationDB(':memory:'); print('OK')"
# Test config loading
python3 -c "import yaml; print(yaml.safe_load(open('config.yaml')))"
# Test in dry run mode (recommended!)
cp config.yaml.example config.yaml
# Edit config.yaml and set dry_run: true
python3 lookup_moderator.pyTest with uv:
# Install dependencies
uv pip install -r requirements.txt
# Test database
uv run python -c "from moderation_db import ModerationDB; db = ModerationDB(':memory:'); print('OK')"
# Test config loading
uv run python -c "import yaml; print(yaml.safe_load(open('config.yaml')))"
# Test in dry run mode (recommended!)
cp config.yaml.example config.yaml
# Edit config.yaml and set dry_run: true
uv run lookup_moderator.pyQuick Dry Run Test: The easiest way to test the tool is with dry run mode enabled. This lets you:
- Verify your configuration is correct
- See what reports are coming in
- Understand what would be deleted without actually deleting anything
- Test without needing strfry installed
See the "Dry Run Mode" section above for detailed examples.
Q: How do I test the tool without deleting anything?
A: Enable dry run mode! Set dry_run: true in the moderation section of config.yaml. The tool will show you what it would do without executing any commands. See the "Dry Run Mode" section above for examples.
Q: Do I need strfry installed to test in dry run mode? A: No! Dry run mode doesn't execute any strfry commands, so you can test the monitoring and reporting logic without having strfry installed.
Q: Do I need to run my own relay? A: Yes, you need a strfry relay to delete content from (but not for dry run testing).
Q: Can I use other relays besides wot.nostr.net? A: Technically yes, but you'd lose the WoT filtering. wot.nostr.net is recommended because it already filters by your trust network.
Q: What if I don't want to auto-delete?
A: Set auto_delete: false in config. The script will log when threshold is met but won't delete.
Q: Can I moderate kinds other than 30817/31990?
A: Yes, add them to events.monitored_kinds in config.
Q: How do I know what's in my WoT on wot.nostr.net? A: wot.nostr.net uses your follow list (kind 3) and follows-of-follows. If you publish a follow list, you're in the network.
Q: Can I run this without publishing delete events?
A: Yes! Just leave private_key empty or set publish_deletes: false. The local deletion will still work.
Q: Should I use pip or uv? A: Either works! uv is faster for installing dependencies and managing Python versions, but pip is more universally available. Use whichever you prefer.
Contributions welcome! Please:
- Fork the repository
- Create a feature branch
- Make your changes
- Submit a pull request
See LICENSE file for details.
- thelookup - Nostr ecosystem directory
- strfry - High-performance Nostr relay
- nostr-sdk - Rust Nostr SDK with Python bindings
- wot.nostr.net - Web of Trust filtered relay
- GitHub Issues: https://github.com/nostr-net/lookup-moderator/issues
Built for the Nostr ecosystem