Overview
You find validated SaaS gaps by searching target subreddits for demand phrases like "is there an app that does X" and "anyone know a tool for", pulling the full threads with comments, and clustering the recurring complaints into candidate product ideas. Scavio's Reddit API gives you two endpoints for this: /api/v1/reddit/search costs 1 credit and returns matching threads, and /api/v1/reddit/post costs 2 credits because it includes the comment tree, which is where the real frustration lives. This replaces a brittle custom scraper that breaks every time Reddit changes its markup or rate-limits your IP. One honest limit up front: Reddit gives you qualitative demand evidence, not market sizing. A thread where forty people pile on about a missing Shopify feature tells you the pain is real; it does not tell you how many people would pay. Validate the size separately with search volume before you build.
Trigger
On-demand discovery run, or weekly cron
Schedule
On-demand / weekly
Workflow Steps
Pick subreddits and demand phrases
Choose the subreddits where your target users complain out loud. For SaaS and app gaps that is usually r/SaaS, r/indiehackers, r/Entrepreneur, plus the niche community you serve, like r/shopify or r/ecommerce. Then write a short list of demand phrases people actually type when they want a tool that does not exist: "is there an app", "anyone know a tool", "why is there no", "wish there was a way to". These phrases are the heart of the workflow because they catch intent, not just topic. Keep the list to five or six; a tight list keeps the credit spend predictable and the noise down.
Search each phrase with /api/v1/reddit/search
Loop your demand phrases through POST /api/v1/reddit/search with a body like {"query": phrase, "sort": "relevance"}. Each call costs 1 credit and returns matching threads with their title, score, subreddit, and permalink. Collect every thread permalink into a set so you do not fetch the same post twice when two phrases hit the same thread. At this stage you are only paying 1 credit per phrase, so casting a wide net here is cheap; the expensive part comes later.
Filter threads worth opening
Not every match deserves a full fetch. Score the candidate threads by upvotes and comment count, and keep the ones above a floor you set, say five comments or a score above ten. A thread with one comment is one person; a thread with thirty is a pattern. This filter matters because the next step costs 2 credits per thread, so you want to spend those credits on threads that actually carry a discussion.
Pull full threads with /api/v1/reddit/post
For each thread that passes the filter, POST to /api/v1/reddit/post with the thread URL. This call costs 2 credits, not 1, because it returns the post body plus the threaded comments. The comments are the payload here: that is where someone names the exact workflow they hate, the tool they tried and dropped, and the price they would pay. Collect the post body and the flattened comment text into one blob per thread.
Extract pain phrases and cluster them
Run the collected comment text through a simple grouping pass. Pull out the sentences that name a missing capability or a complaint about an existing tool, then cluster them by the problem they describe. You can keyword-bucket this, or hand the blob to an LLM with a prompt that asks it to group complaints into distinct product gaps. Each cluster with several independent voices is a candidate idea; a cluster of one is a stray opinion.
Rank candidates and validate size separately
Rank the clusters by how many distinct threads and distinct users mention them, not by total comment count, so one loud thread does not outweigh five quieter ones. The top clusters are your validated gaps. Then do the thing Reddit cannot do for you: take each candidate's core phrase to a keyword tool or Scavio's Google endpoint and check whether real search volume exists. Reddit proves the pain is real; search volume tells you whether enough people are hunting for the fix to make a business.
Schedule it and watch the trend
Run the pipeline once for a snapshot, or put it on a weekly cron so you watch which gaps grow. A complaint that shows up in three threads this month and nine next month is a gap getting worse, which is exactly when a tool sells. Scavio is a data layer and does not schedule anything itself, so the cron or your agent owns the timing. Cache the thread permalinks you have already fetched between runs to avoid paying 2 credits for the same post twice.
Python Implementation
"""
Mine Reddit for validated SaaS gaps.
Search target subreddits for demand phrases (1 credit each),
fetch the meaty threads with their comments (2 credits each),
and collect the comment text for clustering into product ideas.
Reddit is qualitative demand evidence, not market sizing.
Validate volume separately before you build.
Requires: pip install requests
Set SCAVIO_API_KEY in your environment.
"""
import os
import time
import requests
API_KEY = os.environ["SCAVIO_API_KEY"]
BASE = "https://api.scavio.dev/api/v1"
HEADERS = {
"Authorization": f"Bearer {API_KEY}",
"Content-Type": "application/json",
}
# Phrases people type when they want a tool that does not exist yet.
DEMAND_PHRASES = [
"is there an app",
"anyone know a tool",
"why is there no",
"wish there was a way to",
"looking for a tool that",
]
# Only open threads with real discussion (each open costs 2 credits).
MIN_COMMENTS = 5
MIN_SCORE = 10
def post(path, body):
resp = requests.post(f"{BASE}{path}", headers=HEADERS, json=body, timeout=60)
resp.raise_for_status()
return resp.json()
def search_demand(phrase):
# 1 credit per call.
data = post("/reddit/search", {"query": phrase, "sort": "relevance"})
return data.get("threads", [])
def fetch_thread(url):
# 2 credits: returns the post body plus the comment tree.
return post("/reddit/post", {"url": url})
def collect_candidate_threads():
seen = {}
for phrase in DEMAND_PHRASES:
for t in search_demand(phrase):
url = t.get("url") or t.get("permalink")
if not url or url in seen:
continue
comments = t.get("num_comments", 0)
score = t.get("score", 0)
if comments >= MIN_COMMENTS and score >= MIN_SCORE:
seen[url] = t
time.sleep(0.5)
return list(seen.values())
def comment_text(thread):
blob = [thread.get("selftext", "")]
for c in thread.get("comments", []):
body = c.get("body")
if body:
blob.append(body)
for reply in c.get("replies", []):
rbody = reply.get("body")
if rbody:
blob.append(rbody)
return "\n".join(b for b in blob if b)
def mine_gaps():
candidates = collect_candidate_threads()
print(f"{len(candidates)} threads worth opening")
corpus = []
for t in candidates:
url = t.get("url") or t.get("permalink")
thread = fetch_thread(url)
corpus.append({
"title": t.get("title"),
"subreddit": t.get("subreddit"),
"score": t.get("score"),
"source_url": url,
"text": comment_text(thread),
})
time.sleep(0.5)
# Hand `corpus` to your clustering step (keyword buckets or an LLM)
# to group recurring complaints into candidate product gaps.
return corpus
if __name__ == "__main__":
gaps = mine_gaps()
for row in gaps:
print(f"[{row['subreddit']}] {row['title']} ({row['score']})")
print(row["source_url"])
JavaScript Implementation
/**
* Mine Reddit for validated SaaS gaps.
* Search target subreddits for demand phrases (1 credit each),
* fetch the meaty threads with their comments (2 credits each),
* and collect the comment text for clustering into product ideas.
*
* Reddit is qualitative demand evidence, not market sizing.
* Validate volume separately before you build.
*
* Node 18+ (built-in fetch). Set SCAVIO_API_KEY in your environment.
*/
const API_KEY = process.env.SCAVIO_API_KEY;
const BASE = "https://api.scavio.dev/api/v1";
const HEADERS = {
Authorization: `Bearer ${API_KEY}`,
"Content-Type": "application/json",
};
// Phrases people type when they want a tool that does not exist yet.
const DEMAND_PHRASES = [
"is there an app",
"anyone know a tool",
"why is there no",
"wish there was a way to",
"looking for a tool that",
];
// Only open threads with real discussion (each open costs 2 credits).
const MIN_COMMENTS = 5;
const MIN_SCORE = 10;
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
async function post(path, body) {
const resp = await fetch(`${BASE}${path}`, {
method: "POST",
headers: HEADERS,
body: JSON.stringify(body),
});
if (!resp.ok) throw new Error(`${resp.status} ${await resp.text()}`);
return resp.json();
}
// 1 credit per call.
async function searchDemand(phrase) {
const data = await post("/reddit/search", { query: phrase, sort: "relevance" });
return data.threads || [];
}
// 2 credits: returns the post body plus the comment tree.
async function fetchThread(url) {
return post("/reddit/post", { url });
}
async function collectCandidateThreads() {
const seen = new Map();
for (const phrase of DEMAND_PHRASES) {
const threads = await searchDemand(phrase);
for (const t of threads) {
const url = t.url || t.permalink;
if (!url || seen.has(url)) continue;
const comments = t.num_comments ?? 0;
const score = t.score ?? 0;
if (comments >= MIN_COMMENTS && score >= MIN_SCORE) seen.set(url, t);
}
await sleep(500);
}
return [...seen.values()];
}
function commentText(thread) {
const blob = [thread.selftext || ""];
for (const c of thread.comments || []) {
if (c.body) blob.push(c.body);
for (const reply of c.replies || []) {
if (reply.body) blob.push(reply.body);
}
}
return blob.filter(Boolean).join("\n");
}
async function mineGaps() {
const candidates = await collectCandidateThreads();
console.log(`${candidates.length} threads worth opening`);
const corpus = [];
for (const t of candidates) {
const url = t.url || t.permalink;
const thread = await fetchThread(url);
corpus.push({
title: t.title,
subreddit: t.subreddit,
score: t.score,
source_url: url,
text: commentText(thread),
});
await sleep(500);
}
// Hand `corpus` to your clustering step (keyword buckets or an LLM)
// to group recurring complaints into candidate product gaps.
return corpus;
}
mineGaps()
.then((gaps) => {
for (const row of gaps) {
console.log(`[${row.subreddit}] ${row.title} (${row.score})`);
console.log(row.source_url);
}
})
.catch((err) => {
console.error(err);
process.exit(1);
});