Suppress bounced leads and rotate contacts across cold email campaigns.
Named after the CS concept of dead-letter queues -- messages that can't be delivered get routed to a separate queue instead of being retried forever.
Your cold email tool has a 1,000 contact cap. You have 5,000 leads.
You run a campaign, 50 emails bounce, but those bounced addresses stay in the system. Next campaign, you re-target "non-openers" -- but non-openers includes the bounced emails (they also have email_open_count = 0). Your bounce rate spikes, sender reputation drops, deliverability tanks.
Meanwhile, completed campaigns still occupy contact slots. You can't add new leads without manually removing old ones.
- Central lead vault -- one Postgres database with every contact across all campaigns
- Suppression list -- bounced, DND, unsubscribed, and spam complaints tracked across campaigns
- Auto-detection -- syncs campaign data and automatically flags bounced emails
- Suppression guard -- every lead upload is filtered through the vault before reaching Instantly
- Contact rotation -- backup completed campaigns to the vault, free up slots, re-enroll clean leads
# Clone and setup
git clone https://github.com/anianroid/deadletter.git
cd deadletter
bash scripts/setup.sh
# Check what's configured
.venv/bin/python scripts/deadletter.py setup
# Edit .env with your credentials
# DATABASE_URL = any Postgres (Neon free tier works: https://neon.tech)
# INSTANTLY_API_KEY = from Instantly settings > integrations
# Initialize database + import
.venv/bin/python scripts/deadletter.py init
.venv/bin/python scripts/deadletter.py import --instantly -v
# Check stats
.venv/bin/python scripts/deadletter.py statsgit clone https://github.com/anianroid/deadletter.git
cd deadletter
bash scripts/setup.shThen tell Claude: "Set up deadletter for me." It will walk you through connecting your database and Instantly account.
- Open your Cowork server
- Run this in a channel:
Clone https://github.com/anianroid/deadletter.git and set it up for me.
Claude will clone the repo, run the setup script, and guide you through adding your Neon database URL and Instantly API key. No terminal knowledge needed.
If you already have a project and just want the deadletter skill:
# From your project root
git clone https://github.com/anianroid/deadletter.git /tmp/deadletter
cp -r /tmp/deadletter/skills/deadletter .claude/skills/deadletter
cp /tmp/deadletter/scripts/deadletter.py scripts/deadletter.py
cp /tmp/deadletter/scripts/requirements.txt scripts/deadletter-requirements.txt
pip install -r scripts/deadletter-requirements.txtThen add to your project's CLAUDE.md:
## Lead Suppression
See `.claude/skills/deadletter/SKILL.md` for lead vault and suppression guard commands.Once installed, just talk to Claude in plain language:
- "I have a list of leads, make sure none of them are bounced"
- "Add these leads to my Skills Launch campaign"
- "I'm hitting the contact limit, can you free up space?"
- "Is john@example.com safe to email?"
- "Show me my campaigns"
- "Set up deadletter for me"
Claude will translate these into the right commands and explain results without showing UUIDs or raw terminal output.
| Command | Description |
|---|---|
setup |
Check what's configured and what's missing |
campaigns |
List all campaigns by name |
init |
Create database tables |
import --instantly |
Import leads from all Instantly campaigns |
import --csv leads.csv |
Import leads from CSV |
sync |
Pull latest campaign data from Instantly |
suppress |
Auto-detect bounced leads, add to suppression list |
suppress --add email --reason bounced |
Manually suppress an email |
check email@example.com |
Check if an email is suppressed |
guard --csv leads.csv --name "My Campaign" |
Upload leads with automatic suppression filtering |
rotate --dry-run |
Preview which campaigns can free up slots |
rotate --confirm |
Backup completed campaigns, free Instantly slots |
enroll --name "My Campaign" --limit 50 |
Pull clean leads from vault into a campaign |
export --csv leads.csv --clean |
Export non-suppressed leads to CSV |
stats |
Show vault summary |
All commands that accept --campaign-id also accept --name for campaign name matching (partial match, case-insensitive).
# Upload leads with automatic suppression check
.venv/bin/python scripts/deadletter.py guard --csv leads.csv --name "My Campaign" --dry-run
# If the dry run looks good, upload for real
.venv/bin/python scripts/deadletter.py guard --csv leads.csv --name "My Campaign"The guard command checks every lead against the suppression list and campaign history before uploading. Bounced, DND, unsubscribed, and duplicate leads are automatically blocked.
# Make sure vault is current
.venv/bin/python scripts/deadletter.py sync -v
.venv/bin/python scripts/deadletter.py suppress -v
# Enroll clean leads (skips suppressed + already-in-campaign)
.venv/bin/python scripts/deadletter.py enroll --name "My Campaign" --limit 100# Preview what would be freed
.venv/bin/python scripts/deadletter.py rotate --dry-run -v
# Backup to vault + remove from Instantly
.venv/bin/python scripts/deadletter.py rotate --confirm -v# Sync latest data (opens, replies, bounces)
.venv/bin/python scripts/deadletter.py sync --name "My Campaign" -v
# Catch new bounces
.venv/bin/python scripts/deadletter.py suppress -vFour tables in any Postgres database:
- leads -- single source of truth for every contact (dedup on email)
- suppression_list -- emails that must never be contacted again, with reason
- campaign_history -- per-lead-per-campaign record (status, opens, replies)
- campaign_metadata -- campaign-level info (name, lead count, sync status)
See skills/deadletter/references/SCHEMA.md for full column details.
Currently supports Instantly. The architecture is provider-agnostic -- contributions for Smartlead, Lemlist, and other platforms are welcome.
Apache 2.0