Under the hood · built for KKR
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
The request lifecycle
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.
Call
Pipeline stage
Stream
Ingest
GET /api/report/stream · validate · rate limit · budget
Route + junk-name filter
company vs sector · resolve the domain · reject non-companies
Discover (two-pass)
findSimilar + semantic search, merged
Relevance + independence gates
drop name-collisions and parent-owned sub-brands
Emerging discovery
Exa Agent deep research for the long tail
Cluster + tear sheets
one pass into 3-6 segments, evidence-only
Per-company intel + quant
Exa facts, then one batched extraction
Market context
one cited stat, or omitted
Deal thesis
the partner-grade recommendation
Persist
Firestore save → shareable /r/[id]
Stage by stage
The six phases, in detail.
01
Ingest
The expensive route is guarded before any model runs, then opens a streaming response.
- route handler
GET /api/report/streamvalidates 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 asend()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.
- Gemini
routeInput()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, emittinginvalidso 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 contents
fetchSiteContent()reads the anchor's own site to ground everything in what it actually does. - Exa ×2
findSimilaroff the seed domain and a semanticsearchrun 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.
- Gemini
filterRelevant()drops name-collisions (the “Exa” vs “Exaforce” problem), judging by what each company does. - Gemini
filterIndependent()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. - Gemini
synthesizeLandscape()clusters everything into 3-6 segments and writes per-company tear sheets in one pass. Emitssegments. - Exa
fetchCompanyIntel()pulls facts + a recent signal per company (bounded concurrency of 6). - Gemini
extractQuant()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 eachcompany.
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 + Gemini
fetchMarketContext()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. Emitsmarket. - Gemini
analyzeOpportunity()writes theDealThesis: 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. Emitsanalysisthensummary. - Firestore
saveReport()persists the deck with cache keys; Regenerate overwrites the same/r/[id]so share links stay valid. Emitsdone.
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.
The trust layer
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.
The streaming protocol
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 anchorcachedon a cache hit: the existing report id, then the stream endssegmentsafter clustering: the sub-segment mapcompanyonce per surviving company as it is enrichedemergingthe off-database subset, flagged for the Exa Vantage slidemarketthe cited market-size stat, if one was foundanalysisthe deal thesis (recommendation, targets, risks, ask)summarythe one-line executive summarydonebuild complete: generatedAt + the saved report idinvalidthe input is not a company or sector: stop early with a nudgeprogress / errorphase updates throughout, or a graceful failureWhat it runs on
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
Back to How it works · Source on GitHub