DEV Community

Jason Huang
Jason Huang

Posted on

How I Built an Evidence-Backed SaaS Opportunity Pipeline

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.

TL;DR

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
Enter fullscreen mode Exit fullscreen mode

The most important decisions were:

  • Normalize evidence instead of normalizing platform popularity.
  • Use an LLM to interpret signals, not to assign the final score.
  • Keep opportunity quality separate from confidence.
  • Preserve original evidence so every conclusion can be challenged.
  • Treat scheduling as a durable workflow, not a set of loosely timed cron jobs.
  • Treat technical access and permission to use a source as separate questions.

The real problem: signals are not votes

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.

Phase 1: Put every source behind an adapter

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;
}
Enter fullscreen mode Exit fullscreen mode

Each adapter answers four questions:

  1. Is this source currently available?
  2. Which independent streams should be fetched?
  3. How should one page be retrieved?
  4. How should a raw record become a normalized signal?

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

The ingestion runner handles the shared mechanics:

  • Pagination and retries
  • Rate-limit accounting
  • Record validation
  • Deduplication and content hashing
  • Inserted, updated, unchanged, and failed counts
  • Persistence of normalized and raw evidence

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.

Normalize evidence, not meaning

The normalized contract includes shared fields such as:

  • Source and external identifiers
  • Canonical URL
  • Title and content
  • Publication and discovery timestamps
  • Source reliability metadata
  • Engagement or trend context
  • Content hashes
  • Raw source metadata

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.

Phase 2: Use the LLM as an analyst, not a judge

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:

  • The user or customer segment
  • The affected workflow
  • The problem or unmet need
  • Existing workarounds
  • Tool requests and urgency
  • Commercial intent
  • Competition or adoption context
  • Direct excerpts supporting the interpretation

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
Enter fullscreen mode Exit fullscreen mode

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 explains how JSON Schema can constrain compatible models. The application still needs validation, failure states, retry limits, and model-version tracking.

Phase 3: Cluster evidence into opportunities

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:

const clustering = {
  matchThreshold: 0.72,
  reviewThreshold: 0.62,
};
Enter fullscreen mode Exit fullscreen mode

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.

Phase 4: Keep classification separate

Classification answers questions such as:

  • Is this B2B, B2C, or developer-focused?
  • Which industry or workflow does it belong to?
  • Is it a new product, automation layer, vertical tool, or infrastructure?
  • Is the classification confident enough to publish?

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.

Phase 5: Score deterministically

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:

  1. Market pull
  2. Unmet opportunity
  3. Commercial viability
  4. Timing and momentum
  5. Product feasibility
  6. Distribution access
  7. Market whitespace

Missing evidence receives conservative priors instead of optimistic assumptions.

The system also keeps three concepts separate.

Opportunity quality

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

Confidence

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.

Proof maturity

What kind of evidence has actually been observed?

discovery → promising → corroborated → validated
Enter fullscreen mode Exit fullscreen mode

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.

Phases 6 and 7: Publish a shortlist, not the firehose

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.

Why I replaced independent cron timing

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
Enter fullscreen mode Exit fullscreen mode

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.

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

  • Current step
  • Attempts and retry time
  • Cursor and source run identifiers
  • Result summary and error details
  • Lease expiration

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.

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

Lessons that changed the design

More data can make the output worse

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

Platform metrics cannot be combined naively

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.

A healthy ingestion run may accept zero records

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.

LLM output needs operational states

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

Confidence should not be hidden inside quality

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

Technical access is not permission

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.

What the pipeline still cannot prove

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:

  • Total addressable market
  • Customer acquisition cost
  • Willingness to switch
  • Pricing tolerance
  • Founder-market fit
  • A defensible distribution channel

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.

A practical checklist

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

  1. Begin with two complementary sources, not ten.
  2. Write down what each source can and cannot prove.
  3. Define the normalized evidence contract before adding adapters.
  4. Store raw records and canonical URLs for later inspection.
  5. Add source quotas before introducing an LLM.
  6. Require structured output with explicit review and failure states.
  7. Keep scoring deterministic, versioned, and separate from confidence.
  8. Persist workflow progress instead of trusting fixed cron gaps.
  9. Make every published conclusion traceable to evidence.
  10. Measure useful accepted evidence—not raw records collected.

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

What this became

This pipeline now powers GripeRadar, 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.

Top comments (0)