How Exa Vantage works, end to end.

One company goes in. A cited, partner-grade deal-origination deck streams out. Here is the whole backend: every Exa and Gemini call, in order, and what each one streams to your screen.

1

company in

~90s

to a full deck

2

models · Exa + Gemini

~15-25

API calls, streamed live

One pass, top to bottom.

A single streaming request handler drives the whole pipeline. The left rail is the external call that fires at each stage; the right rail is the NDJSON event the browser receives the moment that stage finishes, so the deck assembles live instead of after a long wait.

LegendExaGeminiGooglestreams to client
Google
1

Ingest

GET /api/report/stream · validate · rate limit · budget

opens NDJSON
Gemini
2

Route + junk-name filter

company vs sector · resolve the domain · reject non-companies

meta | invalid
ExaExa
3

Discover (two-pass)

findSimilar + semantic search, merged

Gemini
4

Relevance + independence gates

drop name-collisions and parent-owned sub-brands

Exa Agent
5

Emerging discovery

Exa Agent deep research for the long tail

Gemini
6

Cluster + tear sheets

one pass into 3-6 segments, evidence-only

segments
ExaGemini
7

Per-company intel + quant

Exa facts, then one batched extraction

company…
ExaGemini
8

Market context

one cited stat, or omitted

market
Gemini
9

Deal thesis

the partner-grade recommendation

analysis
Google
10

Persist

Firestore save → shareable /r/[id]

done

The six phases, in detail.

01

Ingest

The expensive route is guarded before any model runs, then opens a streaming response.

  • route handlerGET /api/report/stream validates the query (2-160 chars), enforces a per-IP rate limit (4/min, 40/day, fail-closed), and checks a global hourly/daily budget kill switch.
  • streamIt opens an NDJSON ReadableStream (application/x-ndjson, no buffering) and hands a send() callback to the pipeline.

02

Route + junk-name filter

Gemini decides what you typed, screens out non-companies, and resolves the rest to a real company, with a cache shortcut on both ends.

  • GeminirouteInput() first judges whether the input is a real research subject at all. A clearly non-company input (a personal name, a public figure, gibberish) is rejected here, before any cache or Exa work, emitting invalid so junk stops in ~1s instead of building a deck. The filter is high-precision: it accepts any plausible brand, even unrecognized or chef-named ones (e.g. Peter Chang), and any coherent sector.
  • GeminiFor valid inputs the same pass classifies company vs sector and resolves the official name + domain; Exa search is the fallback when the model has no reliable domain. Emits meta.
  • cacheA Firestore lookup by normalized input then returns a saved deck instantly (unless you hit Regenerate). A second check runs after the name resolves, to catch alternate phrasings.

03

Discover

Two Exa calls, run in parallel, build the candidate universe so we never over-index on a brand token.

  • Exa contentsfetchSiteContent() reads the anchor's own site to ground everything in what it actually does.
  • Exa ×2findSimilar off the seed domain and a semantic search run together; mergeHits() dedupes them. The market-size search is also kicked off here to overlap its latency.

04

Gate

Two Gemini gates keep only companies that are both genuinely relevant and actually acquirable.

  • GeminifilterRelevant() drops name-collisions (the “Exa” vs “Exaforce” problem), judging by what each company does.
  • GeminifilterIndependent() is the first independence layer: it drops sub-brands of a larger parent and the anchor's own subsidiaries, using model world knowledge. Both gates fall back to keeping the set if they would over-filter.

05

Synthesize

The surviving set is enriched into tear sheets, fact-checked against the live web, and sized, with a second independence pass on real evidence.

  • Exa AgentAn emerging pass adds the off-database long tail: agentDiscoverEmerging() (Exa Agent deep research) when enabled, falling back to a freshness-tuned search. New names are deduped and capped.
  • GeminisynthesizeLandscape() clusters everything into 3-6 segments and writes per-company tear sheets in one pass. Emits segments.
  • ExafetchCompanyIntel() pulls facts + a recent signal per company (bounded concurrency of 6).
  • GeminiextractQuant() is one batched pass that reads founding year, funding, stage, headcount, region, and a parent-owner signal, evidence-only. Any company with a detected parent is dropped here, the second independence layer, before it ever streams. Emits each company.

06

Position & persist

A market stat and a strategic thesis turn the set into a recommendation, then the deck is saved as a shareable link.

  • Exa + GeminifetchMarketContext() searches the sector (not the company), ranks credible sources first, and extracts one stat whose source URL must match a retrieved page, or omits it. Emits market.
  • GeminianalyzeOpportunity() writes the DealThesis: recommendation, conviction, why-now, tiered targets, value levers, risks, the ask, and a per-slide takeaway. Conviction is a derived, calibrated band (convictionSignal()) computed from the set, not a free model choice, and the argument is written to match it. Emits analysis then summary.
  • FirestoresaveReport() persists the deck with cache keys; Regenerate overwrites the same /r/[id] so share links stay valid. Emits done.

Two-pass discovery

findSimilarworks off the seed domain and is strong on established lookalikes; the Exa Agent works off the thesis and reaches recently founded names embeddings have not caught up to. They are complementary, and the Agent's hits are deduped against the findSimilar set so it only ever adds the long tail.

The independence gate, twice

A roll-up target has to be a company you can actually buy. Gate one drops known sub-brands up front using model world knowledge; gate two re-checks each company's fetched text for a parent or owner and drops any that slipped through, before they reach the deck.

What runs across every stage.

Determinism

Every Gemini call runs at temperature: 0with a fixed seed, so the same input yields the same deck. The only run-to-run drift comes from Exa's live web results, which are not cached.

Prompt-injection defense

All third-party web text is wrapped in <UNTRUSTED_CONTENT> markers and every system prompt carries a guard instructing the model to treat it strictly as data, never as instructions.

Honesty by construction

No invented revenue, multiples, or market shares. Quant is evidence-only and blank when unknown; the market stat is cited or omitted; the fragmentation index is capped so a maxed set stays believable; every company links to its live site.

Resilience

Exa and Gemini calls retry with exponential backoff, and every stage is best-effort: a failed call returns empty and the deck omits that piece rather than erroring. Firestore is optional, degrading to a non-shareable deck.

Guardrails

Each build fans out to ~15-25 paid calls, so the route is per-IP rate limited and sits behind a global hourly/daily budget kill switch (Upstash when configured, in-memory fallback otherwise).

Concurrency

Discovery and the market search overlap with Promise.all; per-company intel and enrichment run with bounded concurrency; quant is a single batched pass. The ~90s budget is spent in parallel, not in series.

One JSON object per line.

The client reads the NDJSON body and applies each event as it arrives, so segments, companies, the market stat, and the thesis fill in progressively behind the build-progress view.

metaafter routing: mode, restated query, resolved anchor
cachedon a cache hit: the existing report id, then the stream ends
segmentsafter clustering: the sub-segment map
companyonce per surviving company as it is enriched
emergingthe off-database subset, flagged for the Exa Vantage slide
marketthe cited market-size stat, if one was found
analysisthe deal thesis (recommendation, targets, risks, ask)
summarythe one-line executive summary
donebuild complete: generatedAt + the saved report id
invalidthe input is not a company or sector: stop early with a nudge
progress / errorphase updates throughout, or a graceful failure

Exa

exa-js · findSimilar + neural search + Contents + Exa Agent · cited market sources

Google Gemini

@google/genai · gemini-3.5-flash · seed 7 · temperature 0 · structured JSON

Vercel

Functions on Fluid Compute · NDJSON streaming route · rate limit + budget guard

Next.js · React

16 App Router · React 19 · server-rendered shareable /r/[id]

Google Firestore

firebase-admin · saved decks + cache keys for shareable links

PostHog

posthog-js · product analytics