{"slug": "how-i-built-an-evidence-backed-saas-opportunity-pipeline", "title": "How I Built an Evidence-Backed SaaS Opportunity Pipeline", "summary": "A developer built GripeRadar, a multi-source SaaS opportunity pipeline that combines adapters, an evidence model, LLM analysis, deterministic scoring, and durable orchestration. The pipeline ingests signals from sources like Hacker News, GitHub, Google Trends, Product Hunt, and revenue records, but distinguishes between what each signal suggests and what it does not prove. The architecture uses a common adapter boundary to handle provider-specific details, and the system stores both the signal and its bounded meaning.", "body_md": "**A practical look at the adapters, evidence model, LLM analysis, deterministic scoring, and durable orchestration behind GripeRadar.**\n\nI started building GripeRadar in June 2026 because I kept running into the same problem: generating SaaS ideas was easy, but finding convincing reasons to build them was hard.\n\nA complaint on Hacker News might reveal genuine frustration. A growing GitHub repository might show technical momentum. Google Trends can show increasing attention. Product Hunt can reveal launch activity. Revenue data can show commercial behavior.\n\nBut none of those signals means the same thing.\n\nTen complaints do not prove willingness to pay. GitHub stars do not prove unmet demand. Search growth does not prove that a useful product can be built. Revenue proves that someone is making money, but not necessarily that a nearby opportunity is still open.\n\nSo instead of building another idea generator, I built a multi-source research pipeline around a more useful question:\n\nWhat evidence supports this opportunity, what does that evidence actually mean, and what is still uncertain?\n\nThis article explains how the pipeline works, the architectural decisions behind it, and the mistakes I would avoid if I were starting again.\n\nThe pipeline follows seven product phases:\n\n```\nSource adapters\n    ↓\nRaw signal ingestion\n    ↓\nStructured LLM analysis\n    ↓\nOpportunity clustering\n    ↓\nClassification and review\n    ↓\nDeterministic scoring\n    ↓\nDaily report and newsletter\n```\n\nThe most important decisions were:\n\nThe tempting approach is to collect a lot of data, convert every metric into points, and rank the results.\n\nThat produces numbers quickly. It does not necessarily produce useful conclusions.\n\n| Signal | What it may suggest | What it does not prove |\n|---|---|---|\n| Hacker News complaints | Founder or developer pain | Market size or willingness to pay |\n| GitHub stars and issues | Adoption, technical momentum, or product gaps | A commercially attractive market |\n| Google Trends growth | Increasing search attention | Buyer intent |\n| Product Hunt activity | Launch density and category attention | Unmet demand |\n| YouTube comments | Questions, adoption friction, or tool requests | Independent commercial validation |\n| Revenue records | Commercial behavior in a category | That the same product should be copied |\n\nThe pipeline stores both the signal and its bounded meaning.\n\nA GitHub repository stays technical evidence. A search trend stays attention evidence. A revenue record stays commercial evidence. The system can combine them later, but it does not pretend they are interchangeable units.\n\nThis distinction became the foundation of the architecture.\n\nEach provider has different authentication, pagination, rate limits, identifiers, metadata, and failure modes. Letting those details spread through the application would make every new source a pipeline-wide change.\n\nI instead defined a common adapter boundary. The TypeScript interface looks roughly like this:\n\n```\ninterface SignalSourceAdapter<TRaw = unknown> {\n  descriptor: SignalAdapterDescriptor;\n  executionPolicy?: SignalAdapterExecutionPolicy;\n\n  availability(\n    config: SignalIngestionConfig\n  ): AdapterAvailability | Promise<AdapterAvailability>;\n\n  streams(config: SignalIngestionConfig): Promise<SignalAdapterStream[]>;\n\n  fetchPage(\n    context: SignalFetchPageContext\n  ): Promise<SignalAdapterPage<TRaw>>;\n\n  normalize(\n    raw: TRaw,\n    context: SignalNormalizeContext\n  ): ConnectorSignalItem;\n}\n```\n\nEach adapter answers four questions:\n\nA stream might be a keyword, account, channel, trend feed, product category, or API query.\n\nThe ingestion runner handles the shared mechanics:\n\nThe registry currently contains 13 adapters at different maturity levels. Being registered does not automatically mean a source is enabled or included in production scheduling.\n\nSome sources require credentials. Some require an explicit policy review. Some are deliberately disabled because their transport is too fragile. This lets me remove or pause one source without creating another downstream pipeline.\n\nThe normalized contract includes shared fields such as:\n\nHowever, normalization should not erase what makes a source different.\n\nI can store both GitHub stars and YouTube views as engagement metadata, but I should not add them together. They describe different actions, audiences, and levels of commitment.\n\nThe normalized record gives downstream phases a stable technical shape. Source-aware metadata preserves the meaning needed for later interpretation.\n\nRaw signals are noisy. A post can mention a problem without expressing real pain. A repository can be popular without representing a product opportunity. A trend can be driven by news rather than buyer demand.\n\nPhase 2 uses an OpenRouter-compatible model to convert raw signals into structured analyses. It looks for grounded elements such as:\n\nCandidates are ranked before reaching the model, and adaptive source quotas prevent one noisy provider from consuming the entire batch.\n\nEvery response is validated and assigned an explicit state:\n\n```\naccepted\nneeds_review\nrejected\nskipped\nfailed\n```\n\nThat state model proved important. Treating every successfully parsed response as trustworthy would silently pass weak interpretations into clustering.\n\nStructured output helps, but it is not magic. [OpenRouter's structured-output documentation](https://openrouter.ai/docs/guides/features/structured-outputs) explains how JSON Schema can constrain compatible models. The application still needs validation, failure states, retry limits, and model-version tracking.\n\nOne signal rarely deserves its own opportunity.\n\nSeveral posts may describe the same workflow problem using different language. A GitHub issue may support a complaint found on Hacker News. Search growth may add timing context to a problem already supported elsewhere.\n\nThe clustering phase groups compatible analyses while keeping the original evidence links. The current incremental configuration uses two thresholds:\n\n``` js\nconst clustering = {\n  matchThreshold: 0.72,\n  reviewThreshold: 0.62,\n};\n```\n\nA strong match can update an existing opportunity. A borderline match becomes review-worthy instead of being silently forced into a cluster.\n\nCommercial or technical context can strengthen an opportunity, but it should not replace the underlying problem. That prevents the system from discovering a popular technology and reverse-engineering a fictional customer problem around it.\n\nClassification answers questions such as:\n\nClassification is separate from scoring because the two tasks have different failure modes.\n\nA category can be ambiguous even when the evidence is strong. Conversely, an opportunity can be easy to categorize but poorly supported. Combining both decisions into one opaque model response would hide that distinction.\n\nI did not want the final opportunity score to depend on asking an LLM, “How good is this idea from 1 to 100?”\n\nThat answer would be difficult to reproduce, compare, or debug.\n\nThe scoring phase is deterministic and versioned. It evaluates seven source-neutral dimensions:\n\nMissing evidence receives conservative priors instead of optimistic assumptions.\n\nThe system also keeps three concepts separate.\n\nHow attractive does the opportunity appear based on the available evidence?\n\nHow strongly is that conclusion supported?\n\nConfidence considers evidence independence, dimension coverage, longitudinal depth, source reliability, analysis consistency, and completeness.\n\nA promising opportunity can therefore have high quality but low confidence. It may deserve more research, but not yet a build commitment.\n\nWhat kind of evidence has actually been observed?\n\n```\ndiscovery → promising → corroborated → validated\n```\n\nPopularity or freshness alone cannot produce the highest rating. Strong promotion requires several grounded dimensions and no critical anti-signal.\n\nMost importantly, the score is an investigation aid—not a promise of product-market fit.\n\nThe public output is a daily report containing a small set of ranked opportunities.\n\nEach opportunity remains traceable to its supporting evidence. A reader can open the source, inspect the interpretation, and disagree with it.\n\nThat matters because the pipeline creates hypotheses from incomplete public information. Hiding the sources behind a polished summary would create false authority.\n\nThe same report can then feed a newsletter draft. The reporting layer does not independently reinterpret all the raw data; it consumes the scored opportunity contract produced upstream.\n\nMy earlier scheduling model depended too heavily on fixed gaps:\n\n```\n08:00 ingestion\n08:30 analysis\n09:15 clustering\n09:35 scoring\n10:20 report\n```\n\nThis looks orderly until one phase takes longer than expected.\n\nIf ingestion is delayed, analysis may start with incomplete input. If the model provider retries several requests, clustering may find nothing ready. A later report job might still publish using stale opportunities.\n\nA cron schedule tells you when a function starts. It does not prove that its dependencies finished.\n\nThe current design uses one persisted coordinator. A Supabase `pg_cron`\n\njob invokes it through `pg_net`\n\nevery ten minutes. Supabase documents this combination in its [scheduled functions guide](https://supabase.com/docs/guides/functions/schedule-functions).\n\nEach invocation leases and advances at most one bounded unit of work. The database stores:\n\nA crashed invocation can be resumed, and a slow phase can continue across multiple pulses.\n\nThe protected endpoint is implemented as a Next.js Route Handler—the standard App Router mechanism described in the [Next.js documentation](https://nextjs.org/docs/app/getting-started/route-handlers).\n\nThis is not a full distributed workflow engine. It is a deliberately small coordinator that fixes the specific reliability problem I had.\n\nAdding sources increases coverage, but it also increases duplicates, irrelevant trends, platform-specific biases, and model cost. Filtering has to happen in layers.\n\nA vote, star, view, search index, comment, and dollar are not comparable units. They can contribute context to the same opportunity, but their meaning needs to survive normalization.\n\nSometimes a provider responds correctly and every item fails the quality threshold. That is not necessarily a system failure. Provider availability and useful evidence yield are different metrics.\n\n“Parsed successfully” is not the same as “supported by the source.” Accepted, review, rejected, skipped, and failed states made the rest of the pipeline easier to reason about.\n\nA high-potential but weakly supported opportunity is different from a mediocre opportunity backed by extensive evidence. One score cannot communicate both facts honestly.\n\nA public page, token, feed, or browser-visible endpoint does not automatically authorize automated or commercial collection. Source policy belongs in the architecture, not in a note someone hopes to remember.\n\nThis system does not validate an entire business.\n\nPublic evidence is incomplete and biased toward people who post publicly. Silent customers are missing. Enterprise problems may never appear in open communities. Search activity can be distorted by news. Revenue data can lack context.\n\nEven strong cross-source evidence does not automatically establish:\n\nThe output should be treated as a prioritized research queue. The next steps are still customer conversations, landing-page tests, prototype usage, and payment behavior.\n\nIf I were starting this kind of pipeline again, I would keep the first version narrow:\n\nThe hard part is not collecting signals. It is maintaining the boundaries between attention, pain, commercial behavior, technical momentum, and actual proof.\n\nThis pipeline now powers [GripeRadar](https://griperadar.com/), a project for researching SaaS opportunities using public market signals.\n\nThe product is the visible part, but most of the work has been underneath it: source isolation, evidence preservation, model validation, deterministic scoring, retries, policy gates, and making uncertainty visible.\n\nI am still refining the thresholds and evidence model. That is why I wanted to share the architecture now—the interesting questions are not finished.\n\nHow would you handle confidence differently? Would you require cross-source corroboration before ranking an opportunity, or allow strong independent evidence from one source? Which signal types would you trust least?\n\n*Disclosure: AI tools helped with editing and structure. I reviewed and verified the technical content against the current implementation.*", "url": "https://wpnews.pro/news/how-i-built-an-evidence-backed-saas-opportunity-pipeline", "canonical_source": "https://dev.to/jason_huang/how-i-built-an-evidence-backed-saas-opportunity-pipeline-3gmo", "published_at": "2026-08-04 09:25:35+00:00", "updated_at": "2026-08-04 09:41:04.280780+00:00", "lang": "en", "topics": ["developer-tools", "artificial-intelligence", "large-language-models"], "entities": ["GripeRadar", "Hacker News", "GitHub", "Google Trends", "Product Hunt", "YouTube"], "alternates": {"html": "https://wpnews.pro/news/how-i-built-an-evidence-backed-saas-opportunity-pipeline", "markdown": "https://wpnews.pro/news/how-i-built-an-evidence-backed-saas-opportunity-pipeline.md", "text": "https://wpnews.pro/news/how-i-built-an-evidence-backed-saas-opportunity-pipeline.txt", "jsonld": "https://wpnews.pro/news/how-i-built-an-evidence-backed-saas-opportunity-pipeline.jsonld"}}