Ask a model which subreddits your buyers read and you get the famous ones: r/marketing, r/startups, r/SaaS. This page replaces that list with a scored one, built from your buyers’ own questions before anyone on the team posts. It is for the marketing leader told to start on Reddit who has to say where a person’s hours go.
The autumn advice moved from “be on Reddit” to “check your buyers are there first”. Empact Partners, a consultancy running go-to-market work inside B2B software companies, does that check before a partner’s Reddit program starts, as part of our Generative Engine Optimization work: getting a company named when buyers ask AI engines about its category.
The steps below are the ones we run, with Reddit’s Data API doing the counting and one script doing the rest. We ran them on 5 November 2026 on our own category, using the 20 buying questions that the Empact Panel, the AI-visibility measurement we built for our partners, tracks for Empact Partners itself. This is what came back:
Before the first step, you need:
This stops at the list. It does not cover posting, accounts or how a thread gets answered, and it is not a list of the best subreddits for anybody else’s category.
Start From the Questions Your Buyers Type
Step 1: Write the questions file
Make a folder for the run. In any plain text editor, type your buying questions one to a line, the way a buyer types them into Google or ChatGPT, and save the file there as questions.txt. Leave your company’s name out of them: what you are measuring is where people go before anybody has named you. These are the 20 we ran, our panel’s own unbranded questions, unchanged:
our seo lead wants an llm seo partner since ai answers took our traffic
recommend an aeo consultancy for saas
company that helps SaaS startups catch up with competitors in AI answers
which company could help our SaaS brand grow visibility on AI search engines?
what's the best AI engine optimization consultancy?
which company could help our SaaS with generative engine optimization?
we're a post-PMF B2B SaaS and need a GEO consultancy
GEO consultancy to improve our share of voice in AI answers
best AI search consultancy for VC-backed startups
AI search consultancy for scaling from Series A to Series B
best GEO consultancy for startups on a tight budget
need a consultancy to win back saas clicks from ai overviews
company that helps B2B brands get featured in AI Overviews
company that builds mentions and UGC so AI engines recommend our software
company that can make our startup show up in ChatGPT recommendations
consultancy to get our product recommended when people ask AI for the best tools
any good reddit marketing companies
reddit marketing company for B2B brands
reddit marketing consultancy
which company could help our SaaS grow on Reddit?
The wording matters more than the count. A buyer looking for help names a company or a problem, so seven of ours begin “company that” or “which company could”. The people who hear those sentences every week are in sales and support, which is why they are on the needs list and the keyword tool is not.
Step 2: Ask a model for its list
Paste this into ChatGPT or Claude, with the first two sentences rewritten for your company and your buyer:
I run marketing for a company that helps B2B software companies get recommended in ChatGPT and Google AI answers, and grow on Reddit. Our buyers are heads of marketing and founders at B2B SaaS companies.
List 12 subreddits where these buyers ask for recommendations or talk about the problem we solve. Group them as primary (where they spend time every day), secondary, and long tail. One subreddit per line, written as r/name, with a reason of no more than ten words.
We ran it once through Claude Sonnet 5.5 and got twelve communities back, four in each group. Save the names one to a line, without the r/, as models.txt in the folder. The script treats every one of them as a candidate, however little evidence it turns up.
On a partnership the model is the second source. The first is the partner’s own hour with us, where they say which communities their buyers read, which ones they refuse to be seen in and where their competitors are strong. A first run has the model’s list instead, and every step after this one tests it.
Step 3: Save the script and give it your Reddit app’s keys
Copy the script below into a file named shortlist.py, next to questions.txt. Every number on this page came out of it. It runs in six steps, and each writes a file the next one reads.
#!/usr/bin/env python3
"""Score the subreddits your buyers use, from your buyers' own questions.
Run the steps in order. Each one writes a file the next one reads, and each can be run again on its own.
python3 shortlist.py ask Reddit's search, for every question -> threads.csv
python3 shortlist.py askers where the people asking also post -> audience.csv
python3 shortlist.py engines Google, ChatGPT and Google AI Mode -> engines.csv
python3 shortlist.py communities who is a candidate, its size, rules, mods -> communities.csv, rules.md
python3 shortlist.py score everything joined, with your rule verdicts -> shortlist.csv
python3 shortlist.py reading SaaS the first week's reading in one community -> reading.csv
Needs: questions.txt, one buying question per line, written the way a buyer would type it.
models.txt (optional), one subreddit per line that a model or a colleague suggested.
verdicts.csv (before `score`), which you fill in from rules.md: subreddit,verdict,reason
REDDIT_CLIENT_ID and REDDIT_CLIENT_SECRET (an approved Reddit API app) for every step but `score`.
DATAFORSEO_LOGIN and DATAFORSEO_PASSWORD for `engines` only.
Python 3, nothing to install.
"""
import base64, collections, csv, json, math, os, pathlib, re, sys, time, urllib.error, urllib.parse, urllib.request
UA = "subreddit-shortlist/1.0 (buyer research, read-only)"
PAUSE = 1.0 # seconds between Reddit calls, well inside Reddit's limit
PER_QUESTION = 25 # Reddit search results read per question
LOCATION, LANGUAGE = 2840, "en" # United States, English. 2826 is the UK, 2276 Germany.
BUDGET = 2.00 # dollars; `engines` buys nothing past it
SMALL, HUGE = 200, 5_000_000 # members: under SMALL is out; over HUGE stays only if an engine cites it
TOKEN = None
def reddit(path, **params):
"""GET one Reddit API endpoint as JSON, or None when Reddit says the thing is private, banned or gone."""
global TOKEN
if TOKEN is None:
pair = f"{os.environ['REDDIT_CLIENT_ID']}:{os.environ['REDDIT_CLIENT_SECRET']}".encode()
req = urllib.request.Request("https://www.reddit.com/api/v1/access_token", method="POST",
data=b"grant_type=client_credentials",
headers={"Authorization": "Basic " + base64.b64encode(pair).decode(),
"User-Agent": UA})
with urllib.request.urlopen(req, timeout=30) as r:
TOKEN = json.load(r)["access_token"]
url = "https://oauth.reddit.com" + path + "?" + urllib.parse.urlencode({**params, "raw_json": 1})
time.sleep(PAUSE)
try:
req = urllib.request.Request(url, headers={"Authorization": "Bearer " + TOKEN, "User-Agent": UA})
with urllib.request.urlopen(req, timeout=30) as r:
return json.load(r)
except urllib.error.HTTPError as e:
if e.code in (403, 404, 451):
return None
raise
def questions():
return [q.strip() for q in open("questions.txt", encoding="utf-8") if q.strip()]
def read(name):
return list(csv.DictReader(open(name, encoding="utf-8"))) if pathlib.Path(name).exists() else []
def write(name, header, rows):
with open(name, "w", newline="", encoding="utf-8") as f:
w = csv.writer(f)
w.writerow(header)
w.writerows(rows)
print(f"Wrote {name} ({len(rows)} rows).")
# 1 ─ where the questions are asked ────────────────────────────────────────────────────────────────────
def ask():
rows, askers = [], []
for n, q in enumerate(questions(), 1):
found = reddit("/search", q=q, sort="relevance", t="year", limit=PER_QUESTION, type="link") or {}
for rank, child in enumerate(found.get("data", {}).get("children", []), 1):
t = child["data"]
asked = "?" in t["title"]
rows.append([n, rank, t["subreddit"], t["id"], t["title"], time.strftime("%Y-%m-%d", time.gmtime(t["created_utc"])),
t["num_comments"], "yes" if asked else "no"])
if asked and t["author"] not in ("[deleted]", "AutoModerator"):
askers.append([t["author"], t["id"]])
print(f"{n:>2}. {len(found.get('data', {}).get('children', []))} threads for: {q}")
write("threads.csv", ["question", "rank", "subreddit", "thread", "title", "posted", "comments", "asked"], rows)
write("askers.csv", ["author", "thread"], askers) # names stay on your machine; `askers` deletes this file
# 2 ─ where the people asking also post ───────────────────────────────────────────────────────────────
def askers():
found = collections.defaultdict(set)
for r in read("askers.csv"):
found[r["author"]].add(r["thread"])
active, read_ok = collections.Counter(), 0
for i, (author, threads) in enumerate(sorted(found.items()), 1):
page = reddit(f"/user/{author}/overview", limit=100)
items = (page or {}).get("data", {}).get("children", [])
if not items:
continue # suspended, deleted or hidden: not counted either way
read_ok += 1
subs = set()
for it in items:
d = it["data"]
thread = d.get("id") if it["kind"] == "t3" else (d.get("link_id") or "")[3:]
if thread not in threads: # the question that found them does not count
subs.add(d["subreddit"])
active.update(subs)
if i % 25 == 0:
print(f"{i} of {len(found)} askers read")
write("audience.csv", ["subreddit", "askers_active", "askers_read"],
[[s, c, read_ok] for s, c in active.most_common()])
pathlib.Path("askers.csv").unlink() # keep the counts, forget the names
print(f"Read {read_ok} of {len(found)} askers' public activity. askers.csv deleted.")
# 3 ─ what Google ranks and what ChatGPT and AI Mode cite ──────────────────────────────────────────────
API = "https://api.dataforseo.com/v3/"
ENGINES = {"google": "serp/google/organic", "chatgpt": "ai_optimization/chat_gpt/llm_scraper",
"ai_mode": "serp/google/ai_mode"}
def dfs(path, payload=None):
pair = f"{os.environ['DATAFORSEO_LOGIN']}:{os.environ['DATAFORSEO_PASSWORD']}".encode()
req = urllib.request.Request(API + path, method="POST" if payload else "GET",
data=json.dumps(payload).encode() if payload else None,
headers={"Authorization": "Basic " + base64.b64encode(pair).decode(),
"Content-Type": "application/json"})
with urllib.request.urlopen(req, timeout=120) as r:
return json.load(r)
def body(engine, q):
b = {"keyword": q, "location_code": LOCATION, "language_code": LANGUAGE}
if engine == "google":
b["depth"] = 20
if engine == "chatgpt":
b["force_web_search"] = True
if engine == "ai_mode":
b.update(device="desktop", os="windows")
return b
def thread_of(url):
"""(subreddit, thread id) for a Reddit thread or comment link, else None."""
m = re.search(r"reddit\.com/r/([\w-]+)/comments/([a-z0-9]+)", url or "", re.I)
return (m.group(1), m.group(2).lower()) if m else None
def reddit_links(engine, result):
"""Every Reddit thread in one result, in order, with its position. Ads and paid units are skipped."""
out = []
if engine == "google":
for item in result.get("items") or []:
urls = [item.get("url")] + [i.get("url") for i in item.get("items") or [] if isinstance(i, dict)]
for u in urls:
if thread_of(u):
out.append((item.get("rank_absolute"), u))
elif engine == "chatgpt":
ads = {i.get("url") for i in result.get("items") or [] if i.get("type") == "chat_gpt_ad"}
pools = [result.get("sources")] + [i.get("sources") for i in result.get("items") or []]
urls = [s.get("url") for pool in pools for s in pool or [] if s.get("url") not in ads]
out = [(n, u) for n, u in enumerate(dict.fromkeys(urls), 1) if thread_of(u)]
else:
answer = next((i for i in result.get("items") or [] if i.get("type") in ("ai_overview", "ai_mode")), None)
if answer:
parts = [p for p in answer.get("items") or [] if not (p.get("type") or "").startswith("ai_overview_paid")]
urls = [r.get("url") for r in answer.get("references") or []]
urls += [r.get("url") for p in parts for r in p.get("references") or []]
out = [(n, u) for n, u in enumerate(dict.fromkeys(urls), 1) if thread_of(u)]
return out
def engines():
qs = questions()
folder = pathlib.Path("answers")
folder.mkdir(exist_ok=True)
ledger_file = folder / "tasks.json"
ledger = json.loads(ledger_file.read_text()) if ledger_file.exists() else {}
for e, path in ENGINES.items():
todo = [n for n in range(len(qs)) if f"{e}-{n}" not in ledger]
if not todo:
continue
spent = sum(t["cost"] for t in ledger.values())
reply = dfs(path + "/task_post", [body(e, qs[n]) for n in todo])
for n, task in zip(todo, reply["tasks"]):
if task["status_code"] == 20100:
ledger[f"{e}-{n}"] = {"id": task["id"], "cost": task.get("cost") or 0}
ledger_file.write_text(json.dumps(ledger, indent=1))
spent = sum(t["cost"] for t in ledger.values())
print(f"Posted {len(todo)} {e} questions. Spent so far: ${spent:.4f}")
if spent > BUDGET:
sys.exit(f"Budget of ${BUDGET:.2f} passed. Nothing more is bought.")
pending = [k for k in ledger if not (folder / f"{k}.json").exists()]
while pending:
for key in list(pending):
e = key.rsplit("-", 1)[0]
task = dfs(f"{ENGINES[e]}/task_get/advanced/{ledger[key]['id']}")["tasks"][0]
if task["status_code"] in (40601, 40602): # still queued
continue
(folder / f"{key}.json").write_text(json.dumps(task))
pending.remove(key)
if pending:
print(f"{len(pending)} answers still in the queue; checking again in 60 seconds")
time.sleep(60)
rows, answered = [], collections.Counter()
for key in sorted(ledger, key=lambda k: (k.rsplit("-", 1)[0], int(k.rsplit("-", 1)[1]))):
e, n = key.rsplit("-", 1)
task = json.loads((folder / f"{key}.json").read_text())
if task["status_code"] != 20000 or not task.get("result"):
print(f"no result: {e} question {int(n) + 1}: {task['status_message']}")
continue
answered[e] += 1
for pos, url in reddit_links(e, task["result"][0]):
sub, thread = thread_of(url)
rows.append([int(n) + 1, e, pos, sub, thread, url])
write("engines.csv", ["question", "engine", "position", "subreddit", "thread", "url"], rows)
print("Answers collected: " + ", ".join(f"{e} {answered[e]}" for e in ENGINES)
+ f". Spent ${sum(t['cost'] for t in ledger.values()):.4f}.")
# 4 ─ the candidates, their size, their rules and their moderators ─────────────────────────────────────
def signals():
"""Per subreddit (lowercase): questions asked there, questions it ranks for, questions it is cited for,
and the share of askers active there."""
s = collections.defaultdict(lambda: {"asked": set(), "threads": 0, "google": set(), "cited": set(),
"chatgpt": set(), "ai_mode": set(), "askers": 0, "read": 0, "name": ""})
for r in read("threads.csv"):
x = s[r["subreddit"].lower()]
x["name"] = r["subreddit"]
x["asked"].add(r["question"])
x["threads"] += 1
for r in read("engines.csv"):
x = s[r["subreddit"].lower()]
x["name"] = x["name"] or r["subreddit"]
if r["engine"] == "google":
x["google"].add(r["question"])
else:
x["cited"].add(r["question"])
x[r["engine"]].add(r["question"])
audience = read("audience.csv")
for r in audience:
x = s[r["subreddit"].lower()]
x["name"] = x["name"] or r["subreddit"]
x["askers"] = int(r["askers_active"])
for x in s.values(): # every community is measured against all askers read
x["read"] = int(audience[0]["askers_read"]) if audience else 0
return s
def communities():
s = signals()
picked = {l.strip().removeprefix("r/").lower(): l.strip().removeprefix("r/") for l in
(open("models.txt", encoding="utf-8") if pathlib.Path("models.txt").exists() else []) if l.strip()}
for key, name in picked.items():
s[key]["name"] = s[key]["name"] or name
candidates = []
for key, x in s.items():
share = x["askers"] / x["read"] if x["read"] else 0
why = []
if key in picked:
why.append("suggested")
if len(x["asked"]) >= 2:
why.append("asked")
if x["google"] or x["cited"]:
why.append("ranked or cited")
if x["askers"] >= 3 and share >= 0.10:
why.append("askers")
if why:
candidates.append((key, x, why))
rows, notes = [], []
for key, x, why in sorted(candidates, key=lambda c: c[0]):
about = (reddit(f"/r/{x['name']}/about") or {}).get("data", {})
rules = (reddit(f"/r/{x['name']}/about/rules") or {}).get("rules", [])
mods = reddit(f"/r/{x['name']}/about/moderators") or {}
mods = [m["name"] for m in mods.get("data", {}).get("children", [])]
members = about.get("subscribers")
kind = about.get("subreddit_type") or "unreadable"
size = ("" if members is None else "too small" if members < SMALL else
"too big unless cited" if members > HUGE and not x["cited"] else "")
rows.append([about.get("display_name") or x["name"], "; ".join(why), members if members is not None else "",
kind, "yes" if about.get("over18") else "no", size, len(mods)])
notes.append(f"## r/{about.get('display_name') or x['name']}\n\n"
f"{members if members is not None else '?'} members · {kind} · found by: {', '.join(why)}\n\n"
f"{(about.get('public_description') or '').strip()}\n\n"
+ "".join(f"{i}. **{r.get('short_name', '').strip()}** ({r.get('kind')}): "
f"{' '.join((r.get('description') or '').split())}\n" for i, r in enumerate(rules, 1))
+ (f"\nModerators: {', '.join(mods)}\n" if mods else "\nModerators: not listed\n"))
print(f"read r/{x['name']}")
write("communities.csv", ["subreddit", "found_by", "members", "type", "nsfw", "size_rule", "moderators"], rows)
pathlib.Path("rules.md").write_text("\n\n".join(notes), encoding="utf-8")
print("Wrote rules.md. Read it, then write verdicts.csv: subreddit,verdict,reason "
"(verdict: eligible, observe or closed).")
# 5 ─ the shortlist ────────────────────────────────────────────────────────────────────────────────────
def score():
n = len(questions())
s = signals()
verdicts = {r["subreddit"].lower(): r for r in read("verdicts.csv")}
rows = []
for c in read("communities.csv"):
x, v = s[c["subreddit"].lower()], verdicts.get(c["subreddit"].lower(), {})
asked, google, cited = len(x["asked"]) / n, len(x["google"]) / n, len(x["cited"]) / n
buyers = x["askers"] / x["read"] if x["read"] else 0
verdict = v.get("verdict", "unread")
if c["size_rule"]:
verdict = "closed"
total = round(100 * (1 * asked + 2 * google + 3 * cited + 2 * buyers) / 8, 1) if verdict == "eligible" else ""
rows.append([c["subreddit"], c["members"], len(x["asked"]), len(x["google"]), len(x["cited"]),
len(x["chatgpt"]), len(x["ai_mode"]), x["askers"], x["read"],
round(asked, 3), round(google, 3), round(cited, 3), round(buyers, 3),
verdict, v.get("reason", c["size_rule"]), total])
rows.sort(key=lambda r: (r[-1] == "", -(r[-1] or 0), r[0].lower()))
write("shortlist.csv", ["subreddit", "members", "questions_asked", "questions_ranked", "questions_cited",
"cited_chatgpt", "cited_ai_mode", "askers_active", "askers_read",
"asked_share", "ranked_share", "cited_share", "buyer_share", "verdict", "reason", "score"], rows)
# 6 ─ the first week's reading ────────────────────────────────────────────────────────────────────────
def reading(sub):
"""Every thread in one community that the run tied to a question, the ones the engines use first.
Reading only: nothing here posts, votes or messages."""
found = {}
for r in read("engines.csv") + read("threads.csv"):
if r["subreddit"].lower() != sub.lower():
continue
x = found.setdefault(r["thread"], {"how": set(), "questions": set(), "title": r.get("title", "")})
x["how"].add(r.get("engine", "reddit search"))
x["questions"].add(r["question"])
x["title"] = x["title"] or r.get("title", "")
missing = [t for t, x in found.items() if not x["title"]]
for i in range(0, len(missing), 100):
info = reddit("/api/info", id=",".join("t3_" + t for t in missing[i:i + 100])) or {}
for c in info.get("data", {}).get("children", []):
found[c["data"]["id"]]["title"] = c["data"]["title"]
rows = sorted(found.items(), key=lambda kv: (-len(kv[1]["how"] - {"reddit search"}), -len(kv[1]["questions"])))
write("reading.csv", ["thread", "title", "found_by", "questions", "url"],
[[t, x["title"], "; ".join(sorted(x["how"])), len(x["questions"]),
f"https://www.reddit.com/r/{sub}/comments/{t}/"] for t, x in rows])
if __name__ == "__main__":
steps = {"ask": ask, "askers": askers, "engines": engines, "communities": communities, "score": score}
if len(sys.argv) == 3 and sys.argv[1] == "reading":
reading(sys.argv[2])
elif len(sys.argv) == 2 and sys.argv[1] in steps:
steps[sys.argv[1]]()
else:
sys.exit(__doc__)
Your approved Reddit app gives you two values, its id and its secret. Set them in a terminal in the run’s folder, with your own values inside the quotes:
export REDDIT_CLIENT_ID="your-app-id"
export REDDIT_CLIENT_SECRET="your-app-secret"
The script signs in as the app, not as you. An app signed in that way can read public posts and nothing else: it cannot post, vote or send a message. The two lines last as long as the terminal window.
Count Where Each Question Gets Asked
Step 4: Run Reddit’s search for every question
python3 shortlist.py ask
The script sends each question to Reddit’s search exactly as written, keeps the 25 most relevant threads from the past year, and writes one row per thread to threads.csv. Ours took a minute and came back with 500 rows: 455 threads in 166 communities. Counted by how many of the 20 questions each community answered, the top of the list read:
| Community | Questions asked there | Threads |
|---|---|---|
| r/SaaS | 13 | 48 |
| r/b2bmarketing | 12 | 30 |
| r/DigitalMarketing | 11 | 20 |
| r/aeo | 10 | 23 |
| r/microsaas | 9 | 13 |
| r/GEO_optimization | 8 | 20 |
The tail of that file is noise. r/cscareerquestions, r/forhire and r/cofounderhunt each came up for three of our questions, because Reddit matches words like company and startup. Leave them in. The rules step is where a person throws them out, and that costs less than tuning the search.
Without API access, the same count comes from Reddit’s search page: type the question, set the time filter to the past year, and note the community under each of the first 25 results.
Count What Google Ranks and the AI Answers Cite
Step 5: Run the three engines through DataForSEO
In DataForSEO’s dashboard, open API Access and copy your API login and API password. DataForSEO generates the API password itself, so it differs from the one you log in with. When the screen hides it, Send by e-mail mails it to you. Export both in the same terminal:
export DATAFORSEO_LOGIN="you@yourcompany.com"
export DATAFORSEO_PASSWORD="your-api-password"
python3 shortlist.py engines
The script asks Google, ChatGPT with web search on and Google AI Mode every question from the United States. It keeps each Reddit thread in Google’s top 20 and each Reddit thread an answer cites, and writes them to engines.csv. Ours spent $0.0690 and had all 60 answers back within five minutes:
| Engine | Questions with a Reddit thread | Reddit links |
|---|---|---|
| Google, top 20 | 10 of 20 | 42 |
| Google AI Mode, cited | 12 of 20 | 13 |
| ChatGPT, cited | 3 of 20 | 11 |
ChatGPT cited 196 sources across its 20 answers and only 11 were Reddit threads, so it says little about which community matters. AI Mode cites Reddit far more often. The surprise was r/marketing: Reddit’s own search never returned it, and Google ranked it for three questions.
We weight a citation highest. In the watch list the software we built to monitor Reddit keeps for each partner, a community that AI answers cite for the category’s questions is strong evidence and a keyword match is weak. The score in step 9 keeps the same order: cited counts three times, ranked twice, asked once.
Without DataForSEO, type each question into Google and into ChatGPT with search on, and copy every Reddit link into the same columns. Answers in your own logged-in browser come back shaped by your location and your history, which is why we pay a few cents to ask from a clean session.
Count Where the Askers Spend Their Time
Step 6: Read the askers’ public activity
python3 shortlist.py askers
The people whose thread titles ask one of your questions are the nearest thing Reddit has to your buyers. The script reads each one’s last 100 public posts and comments and counts the communities they appear in, minus the thread that found them, into audience.csv. Then it deletes the names, so a second pass starts again from ask.
Expect empty profiles. Of our 163 askers, 88 returned any activity. The other 75 were deleted, suspended or hidden by their owners, which in a category this full of seeded questions is a finding of its own. The shares are taken over the 88, never the 163. Ours took seven minutes, at one Reddit call a second.
| Community | Askers active there, of 88 |
|---|---|
| r/DigitalMarketing | 24 |
| r/SaaS | 21 |
| r/AskMarketing | 16 |
| r/aeo | 14 |
| r/GEO_optimization | 14 |
| r/SideProject | 12 |
| r/SEO | 11 |
| r/indiehackers | 10 |
Read this column for what it adds. r/indiehackers never came up in the search for any of our questions, yet 10 of the askers spend time there. r/AskReddit came in with 9, because everybody posts in r/AskReddit, and the size rule in the next step takes care of it.
On a partnership this column is a consultant’s call, made from the partner’s first hour and from what we know about how each community treats an outside voice. The askers count is the version anyone can run. An audience tool such as SparkToro answers the same question from a panel instead of from the people asking.
Read the Rules Before Anything Gets a Score
Step 7: Pull every candidate’s size and rules
python3 shortlist.py communities
A community becomes a candidate if the model named it, two or more of your questions were asked there, an engine ranked or cited a thread from it, or at least a tenth of the askers are active there. For each one the script writes the member count to communities.csv and the full rules to rules.md. Ours had 81 candidates and took seven minutes.
The script also applies the size rule we use on partnerships: under 200 members is out, and over five million stays only if an AI answer cites it. Five of ours were under 200 members. r/AskReddit and r/Entrepreneur were over five million with no citation.
The moderator list does not come through. Reddit returned none for any of our 81 to an app reading as itself, so the check our consultants make, whether a competitor moderates the community, is a browser job: open the community page and read the moderators panel. Our verdicts for the top communities rest on their descriptions and rules alone.
Step 8: Write a verdict for every candidate
Decide one activity before you read, and read every rule set against that one. Ours was answering buying questions in comments under your own name, saying where you work when your product comes up, with no links. Then open rules.md and give each community one of four verdicts, with the rule that decided it:
Save the verdicts as verdicts.csv, one community a line, in this shape:
subreddit,verdict,reason
SaaS,eligible,Rule 3 allows one disclosed mention every 60 days; rule 11 bars promoting SaaS tools built for promotional outreach
DigitalMarketing,observe,Rules 1 and 3: no posting of services and no self-promotion
EntrepreneurRideAlong,eligible,"Three rules: be respectful, follow Reddit's rules, engage with good intent"
microsaas,off,Micro SaaS founders: they ask these questions and rarely buy a consultancy
A reason with a comma in it goes inside double quotes. Ours broke on exactly that: one reason was cut at its first comma until we quoted it.
The read moved more of our list than any count did. These are the communities with strong signals that ended up somewhere other than eligible:
| Community | What it had | Verdict and the rule |
|---|---|---|
| r/DigitalMarketing | 11 questions asked, 3 cited, 24 askers | Observe: no posting of services, no self-promotion |
| r/marketing | 3 questions ranked, 1 cited | Observe: zero tolerance for self-promotion, permanent ban |
| r/SEO | 4 questions asked, 11 askers | Observe: no brand awareness, no links |
| r/GrowthHacking | 6 questions asked | Observe: a SaaS founder or employee needs the moderators’ permission |
| r/ProductMarketing | 4 asked, 2 ranked | Observe: a post naming your company is removed |
| r/microsaas | 9 questions asked, 8 askers | Off: founders of small products, who rarely buy a consultancy |
Off was our biggest bucket, 32 of the 81. The solo-founder communities ask our questions all day and rarely hire a consultancy, so for us they are off. For a product sold to small teams they would be the best rows on the sheet, which is the judgment no count makes for you, and the reason the verdict column is typed by a person.
What Reddit says
The literal reading of a rule, and the objection to the whole exercise, sit in the same thread:
“People argue about this all the time, but no soliciting and no market research says means no soliciting and no market research.” — u/nofishies, r/b2bmarketing, Mar 2026
“This is basically how to flood reddit with more garbage, which is already killing the platform.” — u/RedneckPaycheck, r/b2bmarketing, Mar 2026
The second is the reason the rules come before the score. A team that ranks communities first and reads the rules afterwards is looking for the line it can get away with. Read first, and the communities that do not want you are gone before anybody has a number to defend.
Reading the rules is the step our consultants never hand to a script on a partnership, because no script reads a rule the way a moderator enforces it.
Score What Is Left
Step 9: Score the eligible communities
python3 shortlist.py score
Each signal becomes a share. Three are shares of the 20 questions: how many were asked in the community, ranked from it or cited from it. The fourth is the share of the 88 askers read who are active in it. The score weights those shares 1, 2, 3 and 2 and puts them out of 100, and only eligible communities get one. The script writes everything to shortlist.csv.
To argue about the weights, which a team should, take the file into Google Sheets with File, then Import, then Upload. Type my_score in Q1, paste this into Q2, fill it down, and change the four weights inside it:
=IF($N2<>"eligible","",ROUND(100*(1*N($J2)+2*N($K2)+3*N($L2)+2*N($M2))/(1*($J2<>"")+2*($K2<>"")+3*($L2<>"")+2*($M2<>"")),1))
The sheet will differ from the script by a tenth on some rows, because the file rounds the shares to three places. A column you leave blank drops out of the divisor, so a team without DataForSEO clears the ranked and cited columns and scores on the other two. The top of ours:
| Community | Asked, ranked, cited (of 20) | Askers (of 88) | Score |
|---|---|---|---|
| r/SaaS | 13, 3, 8 | 21 | 32.8 |
| r/aeo | 10, 1, 1 | 14 | 13.4 |
| r/AskMarketing | 7, 1, 1 | 16 | 12.0 |
| r/b2bmarketing | 12, 1, 1 | 5 | 12.0 |
| r/digital_marketing | 6, 1, 2 | 9 | 11.3 |
| r/GEO_optimization | 8, 0, 0 | 14 | 9.0 |
| r/EntrepreneurRideAlong | 1, 1, 1 | 6 | 5.5 |
| r/seogrowth | 6, 0, 0 | 6 | 5.5 |
r/SaaS earned the top: 13 questions asked, 8 cited by AI Mode or ChatGPT, 21 of the askers. The gap between it and second place is the result.
Below it the scores sit close enough that r/aeo at 13.4 against r/AskMarketing at 12.0 is not worth a meeting. Size decided none of it: r/aeo has 10,755 members, and r/digital_marketing, just below it, has 382,296.
Set the model’s twelve beside the same evidence and the list from step 2 comes apart:
| The model’s pick | Asked, ranked, cited (of 20) | Askers | Verdict |
|---|---|---|---|
| r/SaaS | 13, 3, 8 | 21 | Eligible, 32.8 |
| r/B2BMarketing | 12, 1, 1 | 5 | Eligible, 12.0 |
| r/marketing | 0, 3, 1 | 2 | Observe |
| r/startups | 3, 1, 0 | 3 | Observe |
| r/SEO | 4, 0, 0 | 11 | Observe |
| r/Entrepreneur | 3, 1, 0 | 2 | Closed on size |
| r/content_marketing | 3, 0, 0 | 5 | Observe |
| r/GrowthHacking | 6, 1, 0 | 5 | Observe |
| r/bigseo | 0, 0, 0 | 6 | Eligible, 1.7 |
| r/ProductMarketing | 4, 2, 0 | 1 | Observe |
| r/microsaas | 9, 1, 0 | 8 | Off |
| r/indiehackers | 0, 0, 0 | 10 | Off |
The model knew the famous names and nothing about their rules. That is fair to the model, which was asked for communities and not for permissions, and it is why its list is where a run starts.
Spend the First Week Reading
Step 10: Pull the reading list for the top community
python3 shortlist.py reading SaaS
The last step writes reading.csv: every thread in that community the run tied to one of your questions, with the ones Google or an AI answer uses first. Ours had 52, and 11 of them were tied to Google or an AI answer. A thread can sit in more than one row:
| How the run tied it to a question | Threads in r/SaaS |
|---|---|
| Cited by Google AI Mode | 4 |
| Cited by ChatGPT | 2 |
| In Google’s top 20 | 7 |
| Reddit’s search only | 41 |
The top three were threads asking whom to hire, two of them for AI search work, and AI Mode cites all three. Further down sat “Best AI consulting companies for startups”, which Google ranks, and “Guide to AI Brand Visibility Tracking Tools in 2026”, which AI Mode cites.
That list is the first week: every thread read to the bottom, who answered, what got upvoted, and what a buyer still would not know at the end of it. The script only reads, and nothing on this page posts, votes or sends a message. Posting, the accounts it takes and how a thread gets answered stay with a person who has a history in that community.
On a partnership this list is among the first things a senior consultant at Empact Partners builds, in the same weeks as the audit. The answering waits until the accounts doing it have that history. Then the work runs in the partner’s own channels, and a standing report says which of its threads the engines cite.
Rerun It Before the Quarter Is Planned
The score counts where your questions live. It cannot say whether the people asking them ever buy, which shows months later, in the threads you enter and the signups that follow.
And the cited column moves: on our own list, the answers cited 4 threads at one run and 8 at the next, the same afternoon. Before the quarter’s plan is set, rename the answers folder so the script buys fresh answers, run engines and score again, and give a person’s hours to the communities that hold their citations across both runs.
If you need a Reddit shortlist your team can defend, and people to work it, book a call with me, and let’s see whether it is something we could do together.
