# How I Built an Evidence-Backed SaaS Opportunity Pipeline

> Source: <https://dev.to/jason_huang/how-i-built-an-evidence-backed-saas-opportunity-pipeline-3gmo>
> Published: 2026-08-04 09:25:35+00:00

**A practical look at the adapters, evidence model, LLM analysis, deterministic scoring, and durable orchestration behind GripeRadar.**

I 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.

A 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.

But none of those signals means the same thing.

Ten 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.

So instead of building another idea generator, I built a multi-source research pipeline around a more useful question:

What evidence supports this opportunity, what does that evidence actually mean, and what is still uncertain?

This article explains how the pipeline works, the architectural decisions behind it, and the mistakes I would avoid if I were starting again.

The pipeline follows seven product phases:

```
Source adapters
    ↓
Raw signal ingestion
    ↓
Structured LLM analysis
    ↓
Opportunity clustering
    ↓
Classification and review
    ↓
Deterministic scoring
    ↓
Daily report and newsletter
```

The most important decisions were:

The tempting approach is to collect a lot of data, convert every metric into points, and rank the results.

That produces numbers quickly. It does not necessarily produce useful conclusions.

| Signal | What it may suggest | What it does not prove |
|---|---|---|
| Hacker News complaints | Founder or developer pain | Market size or willingness to pay |
| GitHub stars and issues | Adoption, technical momentum, or product gaps | A commercially attractive market |
| Google Trends growth | Increasing search attention | Buyer intent |
| Product Hunt activity | Launch density and category attention | Unmet demand |
| YouTube comments | Questions, adoption friction, or tool requests | Independent commercial validation |
| Revenue records | Commercial behavior in a category | That the same product should be copied |

The pipeline stores both the signal and its bounded meaning.

A 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.

This distinction became the foundation of the architecture.

Each 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.

I instead defined a common adapter boundary. The TypeScript interface looks roughly like this:

```
interface SignalSourceAdapter<TRaw = unknown> {
  descriptor: SignalAdapterDescriptor;
  executionPolicy?: SignalAdapterExecutionPolicy;

  availability(
    config: SignalIngestionConfig
  ): AdapterAvailability | Promise<AdapterAvailability>;

  streams(config: SignalIngestionConfig): Promise<SignalAdapterStream[]>;

  fetchPage(
    context: SignalFetchPageContext
  ): Promise<SignalAdapterPage<TRaw>>;

  normalize(
    raw: TRaw,
    context: SignalNormalizeContext
  ): ConnectorSignalItem;
}
```

Each adapter answers four questions:

A stream might be a keyword, account, channel, trend feed, product category, or API query.

The ingestion runner handles the shared mechanics:

The registry currently contains 13 adapters at different maturity levels. Being registered does not automatically mean a source is enabled or included in production scheduling.

Some 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.

The normalized contract includes shared fields such as:

However, normalization should not erase what makes a source different.

I 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.

The normalized record gives downstream phases a stable technical shape. Source-aware metadata preserves the meaning needed for later interpretation.

Raw 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.

Phase 2 uses an OpenRouter-compatible model to convert raw signals into structured analyses. It looks for grounded elements such as:

Candidates are ranked before reaching the model, and adaptive source quotas prevent one noisy provider from consuming the entire batch.

Every response is validated and assigned an explicit state:

```
accepted
needs_review
rejected
skipped
failed
```

That state model proved important. Treating every successfully parsed response as trustworthy would silently pass weak interpretations into clustering.

Structured 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.

One signal rarely deserves its own opportunity.

Several 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.

The clustering phase groups compatible analyses while keeping the original evidence links. The current incremental configuration uses two thresholds:

``` js
const clustering = {
  matchThreshold: 0.72,
  reviewThreshold: 0.62,
};
```

A strong match can update an existing opportunity. A borderline match becomes review-worthy instead of being silently forced into a cluster.

Commercial 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.

Classification answers questions such as:

Classification is separate from scoring because the two tasks have different failure modes.

A 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.

I did not want the final opportunity score to depend on asking an LLM, “How good is this idea from 1 to 100?”

That answer would be difficult to reproduce, compare, or debug.

The scoring phase is deterministic and versioned. It evaluates seven source-neutral dimensions:

Missing evidence receives conservative priors instead of optimistic assumptions.

The system also keeps three concepts separate.

How attractive does the opportunity appear based on the available evidence?

How strongly is that conclusion supported?

Confidence considers evidence independence, dimension coverage, longitudinal depth, source reliability, analysis consistency, and completeness.

A promising opportunity can therefore have high quality but low confidence. It may deserve more research, but not yet a build commitment.

What kind of evidence has actually been observed?

```
discovery → promising → corroborated → validated
```

Popularity or freshness alone cannot produce the highest rating. Strong promotion requires several grounded dimensions and no critical anti-signal.

Most importantly, the score is an investigation aid—not a promise of product-market fit.

The public output is a daily report containing a small set of ranked opportunities.

Each opportunity remains traceable to its supporting evidence. A reader can open the source, inspect the interpretation, and disagree with it.

That matters because the pipeline creates hypotheses from incomplete public information. Hiding the sources behind a polished summary would create false authority.

The 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.

My earlier scheduling model depended too heavily on fixed gaps:

```
08:00 ingestion
08:30 analysis
09:15 clustering
09:35 scoring
10:20 report
```

This looks orderly until one phase takes longer than expected.

If 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.

A cron schedule tells you when a function starts. It does not prove that its dependencies finished.

The current design uses one persisted coordinator. A Supabase `pg_cron`

job invokes it through `pg_net`

every ten minutes. Supabase documents this combination in its [scheduled functions guide](https://supabase.com/docs/guides/functions/schedule-functions).

Each invocation leases and advances at most one bounded unit of work. The database stores:

A crashed invocation can be resumed, and a slow phase can continue across multiple pulses.

The 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).

This is not a full distributed workflow engine. It is a deliberately small coordinator that fixes the specific reliability problem I had.

Adding sources increases coverage, but it also increases duplicates, irrelevant trends, platform-specific biases, and model cost. Filtering has to happen in layers.

A 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.

Sometimes 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.

“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.

A high-potential but weakly supported opportunity is different from a mediocre opportunity backed by extensive evidence. One score cannot communicate both facts honestly.

A 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.

This system does not validate an entire business.

Public 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.

Even strong cross-source evidence does not automatically establish:

The output should be treated as a prioritized research queue. The next steps are still customer conversations, landing-page tests, prototype usage, and payment behavior.

If I were starting this kind of pipeline again, I would keep the first version narrow:

The hard part is not collecting signals. It is maintaining the boundaries between attention, pain, commercial behavior, technical momentum, and actual proof.

This pipeline now powers [GripeRadar](https://griperadar.com/), a project for researching SaaS opportunities using public market signals.

The 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.

I am still refining the thresholds and evidence model. That is why I wanted to share the architecture now—the interesting questions are not finished.

How 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?

*Disclosure: AI tools helped with editing and structure. I reviewed and verified the technical content against the current implementation.*
