Ask vs. Search

Two ways to consume the corpus

Agentic search — POST /v1/insights/ask

Send a research question; our agent decomposes it into sub-queries, runs them against the corpus, and curates the evidence. It resolves sources by name — “what has BG2 said about OpenAI?”, “on Invest Like the Best…” — scoping retrieval to that show (across all of its feeds), and finds the episodes where a topic was actually discussed before reading them.

By default the response is evidence-onlysources with answer: null, ~10–30s — the natural mode when you’re combining our evidence with other data in your own generation. Pass "answer": true to also get a written, cited answer (every [n] maps to a source object); it adds ~15–25s, so expect ~25–45s end-to-end.

The agent picks its own filters from the question’s wording: name a show or an episode and it scopes to that source, name an audience (“what do investors think…”) and it filters by speaker role, give a window (“since June”) and it dates the search.

Evidence only (default):

1{"query": "Where do investors split on hyperscaler ASICs versus NVIDIA's GPUs — and who's making the strongest case on each side?"}

Full written answer:

1{"query": "Where do investors split on hyperscaler ASICs versus NVIDIA's GPUs — and who's making the strongest case on each side?", "answer": true}

Scoped to a source by name and tracked over time — no ids needed:

1{"query": "How have the All-In podcast's views on AI capex evolved over time?"}

Streaming

POST /v1/insights/ask/stream delivers the same over SSE — sub-query events as retrieval progresses, then answer deltas when the written answer is on. Events: stage, sub_query, sub_query_result, retrieval_done, sources, answer_delta, done, ping, error.

Each event is a data: line whose JSON carries a type field — there are no named SSE event: frames, so parse the JSON rather than using addEventListener("stage", …). done repeats the complete answer — treat it as the canonical text; answer_delta events are for display.

Reading the response

Every response carries note_type and note: null when evidence was found; "source_not_covered" when a named show or episode isn’t in the corpus (absence means not covered, not that it said nothing); and "no_evidence" when covered ground simply had nothing on the question. Branch on note_typenot covered is never a silent blank. On the stream, the pair rides on the done event. Responses also include took_ms and a small timing debug object.

Individual search — POST /v1/insights/search

If your platform has its own decomposer and orchestrator, this is your endpoint: deterministic, filterable, fast (~1s filter-only; semantic queries take a few seconds — they include a relevance rerank), and every parameter is a hard guarantee rather than an agent’s judgment call. Your agents compose the sub-queries; you own ranking fusion and synthesis.

Sentiment filtering — three rules

  1. sentiments together with tickers matches as a pair on one mention: “bearish about NVDA”, never bearish-about-something-else while mentioning NVDA.
  2. sentiments alone means “has that sentiment about anything” — a browse of bearish takes, not a claim about a specific company.
  3. Lists combine as any-of. For different stances on different companies — bullish NVDA or bearish AMD in one call — use mentions pairs:
1{
2 "query": "AI chip competition",
3 "mentions": [
4 {"ticker": "NVDA", "sentiment": "bullish"},
5 {"ticker": "AMD", "sentiment": "bearish"}
6 ]
7}

Worked examples

Evidence pack — bearish takes on NVDA from professional investors, with verbatim grounding:

1{
2 "query": "NVIDIA downside risks",
3 "tickers": ["NVDA"],
4 "sentiments": ["bearish"],
5 "speaker_roles": ["investor"],
6 "date_from": "2026-07-18",
7 "include_quotes": true
8}

Follow one voice — everything a show has said on a topic (idl2iow9 is The Real Eisman Playbook, resolved via the directory):

1{"query": "regional banks credit risk", "podcast_ids": ["idl2iow9"]}

Pagination

The response envelope is always {"results": [...], "next_cursor", "pagination_end", "took_ms"} — one page per call, with pagination_end null while more pages exist. For the next page, send next_cursor back by itself:

1{"cursor": "cznqOucaYJWs9XCc..."}

Pages never overlap or reshuffle; cursors expire after ~15 minutes. When next_cursor is null, pagination_end says why: end_of_results (a filter-only browse fully paged the matching window) or ranking_exhausted (a relevance query reached its quality boundary at ~150 results — narrow the query if you need more depth).