You already ran research on your deployed fork. This page explains what you saw, maps the repo, and traces one request through the code. Everything still runs in a single web service.
What the app does
You type a ticker (or any research question). The backend runs four Exa web searches in parallel, then Claude writes a structured memo from the combined results.
In the UI:
- A text box and a submit control.
- Four search cards (price, news, commentary, risks). Each card moves from idle → running → done or failed.
- An activity log with short status lines.
- A memo area that fills in while Claude streams the answer.
The tasks/ folder holds search and synthesis logic, but it is compiled into the same Node process as Express for this baseline. No second service yet.
Repo layout
| Path | Role |
|---|---|
ui/ | React UI. Posts to /api/research, listens on SSE for progress. |
server/ | Express API and the live run registry (runner.ts). |
tasks/src/queries.ts | Builds the four search queries for a ticker. |
tasks/src/search.ts | One Exa search (and the simulated failure helper). |
tasks/src/research.ts | Fans out four searches, then calls synthesis. |
tasks/src/synthesize.ts | Claude memo from indexed sources. |
render.yaml | Blueprint for the web service only. |
Follow one request in the code
Open your clone on disk and trace the same path you triggered in the browser.
Browser to server
- The UI sends
POST /api/researchwith a body like{ "query": "TSLA" }. server/src/runner.tsstartResearch(query)creates arunId, stores listeners, and startsresearch(query, onEvent)fromtasks/src/research.ts. It does not wait for the full pipeline before responding.- The HTTP response is
{ "runId": "…" }. - The UI opens
GET /api/research/:runId/eventsasEventSource. Each SSE message is oneResearchEvent(search card updates, log lines, memo chunks).
Inside research()
Open tasks/src/research.ts.
Fan-out. buildQueries(query) in queries.ts returns four specs: price, news, commentary, risks. The first event is { type: 'started', query, queries: [...] }.
Four searches in parallel (same Node process):
const results = await Promise.all( searches.map(async (spec, index) => { onEvent({ type: 'search:running', index }) const result = await searchOne(query, spec, index) onEvent({ type: 'search:done', index, articleCount: result.articles.length }) return result }),)Each searchOne call hits Exa from the web service process.
Fan-in. buildIndexedArticles emits { type: 'sources', sources }. synthesize() streams the memo: synthesizing, synthesis:chunk, then { type: 'done', memo }.
On this page
- Four parallel Exa searches, then a streamed Claude memo in one web service
- UI driven by POST /api/research and SSE ResearchEvent messages
- research() fans out with Promise.all, then fans in through synthesize()