A small CLI for finding warm prospects on Reddit — people who have already declared intent about the problem your product solves, surfaced from the comments and threads they've written themselves.
Cold DMs are a tax on attention. This tool inverts that: it reads public Reddit activity, scores users against an ICP you define in YAML, and hands you a ranked shortlist with the exact comment that triggered the match — so your first message can refer to something they actually said.
Output flows two places at once:
- A Notion database (your working tracker — one row per prospect)
- A local markdown file at
outputs/prospects_YYYYMMDD.md(audit log + a single human-readable artifact for each run)
ICP definition, signal weights, and the DM voice template are all configuration. There is nothing about the tool that is specific to a single domain — point it at any subreddit ecosystem and any set of intent keywords.
- What it does
- Who it's for
- Install
- Setup walkthrough
- Configure your ICP
- Run
- Output
- How it works
- Project layout
- Limits
- License
- Searches the subreddits you list for comments matching your intent keywords, within a configurable time window.
- Pulls each commenter's recent activity — last 25 comments + 10 posts, account age, and karma — cached on disk so re-runs are cheap.
- Scores them against a YAML-defined ruleset of strong/medium signal patterns, with per-rule caps. Disqualifying conditions (account too new, too much self-promotion, etc.) filter the list.
- Generates a personalized opener for each top candidate using Gemini,
grounded in a DM template you write (
outreach.md). - Writes the top N to your Notion tracker and to a dated markdown file.
The tool does one thing and exits. No daemon, no web UI, no scheduler.
People doing customer development or outbound sales who want to build a list of warm prospects they can reach out to as peers — not strangers. Useful for:
- Indie SaaS founders looking for early users
- Consultants whose ICP gathers in specific subreddits
- Researchers building qualitative interview lists
- Anyone tired of cold-DMing people who never asked
git clone <repo-url> reddit-prospect-miner
cd reddit-prospect-miner
python3 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
cp .env.example .env
cp config.example.yaml config.yamlThen edit .env with your credentials and config.yaml with your ICP. See
the next two sections.
- Sign in to Reddit and go to https://www.reddit.com/prefs/apps/.
- Click create another app... at the bottom.
- Fill in:
- name: anything (e.g.
prospect-miner) - type: select script
- redirect uri:
http://localhost:8080(required field, not actually used for script apps) - leave the other fields blank
- name: anything (e.g.
- Click create app.
- You'll see your app listed. The client ID is the short string under the app name (about 14 characters). The client secret is the longer string labeled secret.
- Paste both into
.env:REDDIT_CLIENT_ID=<short string> REDDIT_CLIENT_SECRET=<longer string> - Set a descriptive
user_agentin yourconfig.yamlunder thereddit:section — Reddit's API guidelines require it to identify you.
The miner uses Reddit in read-only mode, so no OAuth flow is needed.
- Go to https://www.notion.so/profile/integrations and click + New integration.
- Give it a name (e.g.
Prospect Miner), associate it with your workspace, and submit. - Copy the Internal Integration Token (starts with
secret_orntn_) and paste into.env:NOTION_API_KEY=<your token> - Open your prospects database in Notion. Click the
···menu in the top-right → Connections → search for your integration name → Confirm. Without this step the API will return a 404, even with a valid token. - Find your database's ID. Open the database as a full page; the URL looks
like:
The 32-character hex string before the
https://www.notion.so/<workspace>/<database-id>?v=<view-id>?is the ID (with dashes added automatically:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx). - Paste that ID into
config.yamlunderoutput.notion_data_source_id.
Database schema: the miner expects ten properties on your database, listed
under notion.fields in config.example.yaml. The property types can be
whatever fits your workflow (e.g. Status can be a select, status, or
text property — the writer detects the type and adapts). If your existing
database uses different property names, update notion.fields in your
config to map to your names instead.
The ten expected fields:
| Default name | Recommended type | What gets written |
|---|---|---|
Name |
title | u/<reddit_username> |
Platform |
select / text | Reddit |
Link |
url | profile URL |
Product Focus |
select / text | from notion.defaults.product_focus |
Source Type |
select / text | Intent capture - Reddit |
Original Signal |
rich text | trigger permalink + first 200 chars of comment |
Status |
status / select | Identified (move to Contacted after DM) |
Next Action |
select / text | Send Reddit DM |
Next Action Date |
date | today + 1 weekday |
Notes |
rich text | structured markdown: score, why-fit, opener, context |
The miner skips creating a row if a row with the same Name already exists.
By default the miner uses Gemini to write a personalized opener for each
prospect, grounded in the DM template you keep in outreach.md.
- Sign in at https://aistudio.google.com/apikey and create an API key.
- Paste into
.env:GEMINI_API_KEY=<your key> - Edit
outreach.mdto reflect your real DM voice and structure. The LLM reads this file as a reference for how you want openers to sound.
If you'd rather skip the LLM step, set opener.enabled: false in your config
(or pass --skip-opener). The Notes field will then contain a fallback
showing the trigger comment, and you write the opener yourself.
config.example.yaml ships with a generic "indie SaaS founders interested in
cold email" ICP — it's there so you can see the shape and run a dry test. Copy
it to config.yaml and edit:
target.subreddits— where your ICP hangs outtarget.intent_keywords— phrases people use when they have the problemtarget.time_window_days— how far back to look (Reddit's search is coarse; 90 is a good default)scoring.strong_signals/medium_signals— regex patterns that characterize a good-fit user. Each has aweight; medium signals can have acapto prevent one pattern from dominating.scoring.disqualifying— conditions likeaccount_age_days < 60orself_promo_ratio > 0.4that exclude a candidate entirely. Supportsfield op numberexpressions against this namespace:account_age_days,total_karma,comment_count,post_count,self_promo_ratio.output.top_n— how many candidates to write per run (30 default)
source .venv/bin/activate
python -m reddit_prospect_miner --config config.yamlUseful flags:
# Dry run — no Notion writes, no LLM calls
python -m reddit_prospect_miner --skip-notion --skip-opener
# Smaller top-N for testing
python -m reddit_prospect_miner --top-n 5
# Verbose (PRAW + httpx debug)
python -m reddit_prospect_miner -vThe first run takes longer because nothing is cached. Subsequent runs against
the same usernames within the same day re-use cached histories from
cache/<username>_YYYYMMDD.json.
Each run produces:
- Notion rows — one per qualifying top-N prospect, idempotent (dedups by
Name). outputs/prospects_YYYYMMDD.md— the same content as a single scannable markdown file. Per candidate: score, why-fit, signals breakdown, trigger comment in a blockquote, suggested opener, profile/karma context.
Stdout prints progress as the run proceeds, plus a summary at the end (triggers seen / unique users / scored / qualifying / Notion results).
intent search (subreddit × keyword)
│
▼
trigger comments ─► dedupe by author
│
▼
fetch history (cached) ─► Candidate(trigger, history)
│
▼
score against rules ─► ScoredCandidate(score, hits, disqualifications)
│
▼
rank → top N
│
▼
Gemini opener (template-grounded) ─► Prospect(scored, opener)
│
├──► Notion writer (schema-aware, deduped)
└──► Markdown writer (outputs/prospects_YYYYMMDD.md)
Each module is independent and small (~150 LOC). The scoring engine has no
Reddit knowledge; the Notion writer has no scoring knowledge. Productization
is a matter of swapping notion_writer for a CRM writer, or putting the same
pipeline behind a job runner.
reddit_prospect_miner/
__init__.py
__main__.py # python -m reddit_prospect_miner
cli.py # argparse + orchestration
config.py # YAML loader, typed dataclasses
models.py # Candidate, ScoredCandidate, Prospect, ...
reddit_client.py # PRAW wrapper, search, history fetch, on-disk cache
scoring.py # signal matching + disqualification engine
opener.py # Gemini opener generation, with safe fallback
notion_writer.py # schema-aware Notion writer + dedupe
markdown_writer.py # local audit log
tests/
test_scoring.py
test_config.py
config.example.yaml # generic ICP example
outreach.md # your DM template (LLM voice reference)
.env.example
requirements.txt
LICENSE # MIT
- Reddit search is coarse. PRAW's
subreddit.search()returns submissions matching the query, then the miner walks those threads' comments. Comments in threads whose submission doesn't match the keyword will be missed. For most ICPs this is fine — people who declare intent tend to do so in threads about the topic. - Rate-limited. Default
rate_limit_seconds: 1.1between Reddit calls. A full run over 5 subreddits × 13 keywords takes 5–15 minutes for the search phase, plus ~1s/user for history. - Gemini cost. One LLM call per top-N candidate. With
top_n: 30and a500-token prompt, the cost is negligible ($0.01/run ongemini-2.5-flash) but not zero. - Notion writes are not transactional. If the script dies mid-write,
re-running is safe — dedupe by
Nameprevents duplicates.
MIT — fork it, modify it, run it commercially, build a product on top of it. Attribution appreciated but not required.