💡 Inspiration

Markets don't move in straight lines. They move in cascades. Picture a Tuesday across three time zones:

05:42 GMT, Hsinchu. A 50,000-volt substation feeding TSMC's Fab 18 trips. Six hours of cold tools.

18:11 CET, Wolfsburg, three weeks later. Volkswagen pauses the ID.4 line. A press release blames "logistics."

23:08 GMT, Bab-el-Mandeb. A drone clips a Suezmax. Forty-eight hours later, a snack maker in Ohio quietly raises full-year guidance — palm-oil futures spiked and their hedges paid out.

Three apparently unrelated tape-bombs, one causal graph. That graph lives, today, inside the heads of sell-side analysts, in locked Bloomberg chats, and in PDFs nobody opens until Monday. By the time the second-order names move on the screen, the alpha is somebody else's.

I built CascadeTerminal because the instrument that draws this graph in real time doesn't exist outside an institutional desk, and a retail trader (or a curious engineer, or a generalist PM) deserves one.

🔭 What CascadeTerminal is

A terminal — the kind of artefact you leave open on a second monitor — that watches the world in real time and, the moment something breaks, draws you the cascade. Root event. Suppliers. Customers. Peers. The transmission mechanism. A historical analog. The why of every edge, in one sentence per edge.

Open cascade-terminal.vercel.app. A 3D globe is pulsing with everything that hit the wire in the last hour — SEC 8-Ks, breaking news, M≥5 earthquakes, severe-weather alerts, diverted flights, stalled tankers. Click any pulse. In under two seconds the right rail draws the 3-hop cascade tree:

root → suppliers → customers → peers

Each node carries a directional confidence score and a sentence of reasoning written by a Gemini 3 specialist.

Type "earnings misses that crashed semis last quarter" into the search bar; the answer is a list of past cascades ranked by narrative similarity, not ten blue links. Drag a chart PNG or a 10-K PDF onto the same bar and multimodal embeddings find the events that look like your image.

The hardest single move is tickerless events — a typhoon doesn't ship with $NVDA attached. Those route through a Gemini 3 Pro Coordinate Mapping System: one structured-output call infers affected regions, sector exposure, transmission mechanism, and a historical analog, then fans validated arcs from event lat/lon to the listed-equity HQs it expects to repaint next.

One MongoDB Atlas cluster does the data work. One Gemini 3 family does the reasoning. Solo build.

🪟 The surface

Ten visible features, each wired to a real piece of MongoDB, Voyage, or Google infrastructure underneath.

Surface What it does What's underneath
🌐 3D live globe Geo-located events from the last hour pulse in real time; arcs overlay tickerless cascades. react-globe.gl + Three.js, fed by SSE off a MongoDB change stream.
🌲 3-hop cascade tree Click an event; walk supplier / customer / peer / sector edges out three hops, ranked by narrative strength. A single $graphLookup over 1,149 hand-seeded edges + Voyage rerank-2.5.
🗺️ Coordinate Mapping System Tickerless events (typhoons, coups, tariffs) become structured regions + sectors + transmission + analog. Gemini 3 Pro JSON-mode + Pydantic + server-side lat ∈ [-90, 90], lon ∈ [-180, 180] validation.
🧠 Agent Society Five specialist agents collaborate on every cascade: Synthesizer, Critic, Predictor, Memory, Researcher. Google ADK + Gemini 3 Pro orchestrator + Gemini 3 Flash specialists + MongoDB MCP server.
🔍 Hybrid semantic search "earnings misses that crashed semis" returns ranked cascades, not links. $facet( $vectorSearch ⊕ Atlas $search ) → RRF → Voyage rerank-2.5.
🖼️ Multimodal search Drop a chart PNG or 10-K PDF; get the events that match. voyage-multimodal-3.5 against the same vector index.
Time-machine Replay any day in the last 14, frame by frame. TTL-bounded events + native time-series prices.
Counterfactual overlay "What if this event had not happened?" — diff view over cascade nodes. Memory agent + cached cascades + UI store flip.
🪞 Compare mode Pin two events; render two cascade graphs side by side. Zustand compareIds, dual $graphLookup calls.
🩺 Performance Advisor panel The agent surfaces Atlas's own index suggestions in the UI. MongoDB MCP server performance-advisor tool.

Behind the surface sit two halves — a feeder pipeline and a reasoning brain, both pointed at the same Atlas cluster.

🛰️ Ingestion — eight workers, one schema

The ingestion layer isn't a script. It's eight independent worker agents, each a small Cloud Run Job that owns one source and one verb-chain: fetch → normalise → embed → insert. Any one can fail — rate limit, vendor outage, malformed payload — and the other seven keep filling the brain.

Worker Source What it does each run
📜 sec-edgar SEC EDGAR Pulls fresh 8-K / 10-Q / 10-K filings, extracts item-numbered sections, tags tickers from the filer index, normalises to the common Event schema. Sends User-Agent: Cascade research/contact@example.com — without it, EDGAR throttles to zero.
📰 marketaux Marketaux Financial news + sentiment + hero image. The hero image is routed to voyage-multimodal-3.5 so charts in articles become part of the searchable index.
💬 alpha-vantage Alpha Vantage A second-opinion sentiment signal so the cascade rerank never depends on a single vendor's mood.
🌐 gdelt GDELT 2.0 Geopolitics firehose — tariffs, sanctions, coups, infrastructure incidents. Mostly tickerless, flagged for the Coordinate Mapping System downstream.
🌋 usgs USGS Earthquakes ≥ M5.0 with epicentre lat/lon.
🌪️ noaa NOAA Severe weather (hurricanes, ice storms) tagged with the affected region.
✈️ opensky OpenSky Diverted, grounded, or anomalously low-altitude commercial flights near critical infrastructure.
📈 yfinance yfinance OHLCV bars into the native time-series collection — the only worker that writes to prices rather than events.

Three properties they share, learned by getting bitten:

  • Stateless — no per-source database, no local cache. The cluster is the only memory.
  • Idempotent on event hash — each worker computes a stable external_id from source + native_id + published_at and upserts on it, so the same EDGAR filing seen on three consecutive scheduler invocations lands exactly once.
  • Isolated — a 429 from Marketaux does not page out SEC EDGAR. Each worker is its own container with its own retry budget.

📥 How a payload lands in MongoDB

Eight workers, eight vendors, one canonical document shape:

{
  "_id":          ObjectId(...),
  "external_id":  "edgar:0001045810-25-000123",  # dedup key, unique index
  "source":       "sec-edgar",
  "kind":         "filing" | "news" | "quake" | "weather" | "aviation" | "vessel" | "social",
  "tickers":      ["NVDA"],                      # may be empty → Geo-Cascade path
  "sector":       "Semiconductors",
  "headline":     "NVDA · 8-K · Item 2.02 — Q3 results",
  "text":         "...full normalised body...",  # Auto-Embedded server-side
  "entities":     ["TSMC", "Hsinchu", "HBM3"],   # NER pass before insert
  "geo":          {"type": "Point", "coordinates": [120.99, 24.77]},
  "impact":       "high",                        # low | medium | high — drives change stream
  "published_at": ISODate("2026-05-31T13:42:11Z"),
  "fetched_at":   ISODate("2026-05-31T13:42:47Z"),
  "ttl_at":       ISODate("2026-06-14T13:42:11Z")  # +14d, TTL drops it
}

A single events collection holds documents from all eight workers. That uniformity is what makes the downstream cascade query trivial — $vectorSearch, $graphLookup, and Atlas $search all read from one collection regardless of whether the event started life as an SEC filing or an earthquake.

The part that took me a while to internalise: the moment the worker calls

await events.update_one(
    {"external_id": doc["external_id"]},
    {"$setOnInsert": doc},
    upsert=True,
)

five different things happen inside Atlas, roughly in that many milliseconds:

  1. Atlas Automated Embedding runs voyage-4 over the text field server-side and writes a 1024-d vector into a hidden field. The worker never touches an embedding API; the vector is there by the time update_one returns.
  2. The vector index (cosine, 1024-d) incorporates the new embedding — the document is searchable by $vectorSearch immediately.
  3. The Atlas Search index over tickers, entities, and headline picks up the document for exact lexical match.
  4. The TTL index on ttl_at schedules the document for automatic deletion 14 days out. M0's 512 MB cap is enforced by the database, not by a cron I had to remember to write.
  5. If impact == "high", the change stream
   db.events.watch([{"$match": {"fullDocument.impact": {"$in": ["high"]}}}])

— wakes the FastAPI SSE endpoint, which pushes the new document to every connected browser as a Server-Sent Event. The globe lights up. No polling, no webhooks, no glue.

The yfinance worker is the one exception. It writes OHLCV bars to a native time-series collection (prices) with timeField: t and metaField: ticker. That's what gives the time-machine its frame-by-frame replay without a paid-tier index.

End-state: eight independent processes, one canonical document, one collection, and every Atlas capability that touches an event wired up at the moment of insert — embedding, vector index, lexical index, TTL, change stream. The reasoning side doesn't have to ask for anything; everything it needs is in place by the time a cascade query fires.

🧠 The brain — a society of agents

If ingestion makes sure every event lands somewhere queryable, the brain turns a query into a cascade. It is not one large LLM call. It's five.

A real research desk isn't one person. It's an analyst, a sceptic, a forecaster, a historian, and somebody whose job is to chase down the actual source. CascadeTerminal's brain is built the same way — five Gemini 3 agents, each with one job, fanned out in parallel.

                       ┌─────────────────────────────────┐
                       │  Cascade Synthesizer (Gemini 3 Pro)
                       │  · hybrid search + $graphLookup
                       │  · MongoDB MCP tools as its hands
                       └────────────────┬────────────────┘
                                        │
        ┌────────────────┬──────────────┼──────────────┬────────────────┐
        ▼                ▼              ▼              ▼                ▼
  ┌──────────┐   ┌──────────┐   ┌──────────┐   ┌──────────┐    ┌──────────────┐
  │  Critic  │   │ Predictor│   │  Memory  │   │Researcher│    │  Performance │
  │ G3 Flash │   │ G3 Flash │   │ G3 Flash │   │ G3 Flash │    │   Advisor    │
  │ challenge│   │ next-hop │   │ analog   │   │ evidence │    │  (MCP tool)  │
  │  weak    │   │  moves   │   │  search  │   │  trail + │    │              │
  │  links   │   │ + δ-σ    │   │ ($vSrch) │   │ citations│    │              │
  └─────┬────┘   └─────┬────┘   └─────┬────┘   └─────┬────┘    └─────┬────────┘
        │              │              │              │               │
        └──────────────┴──────────────┴──────────────┴───────────────┘
                                        │
                                        ▼
                          fan-in → cached cascade document
                            in MongoDB `cascades` collection

Cascade Synthesizer · Gemini 3 Pro. The only agent that talks to the user's question directly. It calls $vectorSearch and Atlas $search through the MongoDB MCP server, fuses with RRF, walks $graphLookup three hops out from the root ticker, scores candidates with Voyage rerank-2.5, and writes a draft cascade with a one-line why on every edge. Then it fans the draft to the four specialists in parallel.

Critic · Gemini 3 Flash. Asks the most useful question on any research desk: which links are weakest, and what would falsify them? Returns confidence haircuts and counter-arguments.

Predictor · Gemini 3 Flash. Projects each first-order node's next five-day directional move (up / flat / down) with a confidence score, grounded in the relationship type — "TSM is a sole-source foundry; supply shock is mechanically priced within 48 hours." That's the rail of arrows in the cascade tree UI.

Memory · Gemini 3 Flash. $vectorSearch over embedded cascade summaries surfaces structural analogues — "this looks like the March 2011 Tōhoku supply shock to the auto sector."

Researcher · Gemini 3 Flash. Builds the evidence trail. For every edge in the final cascade it goes back to events via $vectorSearch + Atlas $search through the MongoDB MCP server and attaches the concrete source documents — the 8-K paragraph, the news article, the USGS quake record, the GDELT incident — that justify that edge. The cascade becomes auditable.

A society instead of one big call buys two things: specialisation (a focused Flash prompt out-performs a kitchen-sink Pro prompt) and graceful degradation (if any one specialist times out, a deterministic local stub fills that role and the rest of the society still ships). Every agent reaches Atlas through the official MongoDB MCP server — no custom tool wrappers, no raw credentials in LLM context.

🍃 MongoDB — thirteen capabilities, each load-bearing

Both halves of the system terminate on the same Atlas cluster. The rule I held myself to was that every Atlas capability listed has to do real work — pull any one out and a feature breaks.

# Capability Role
1 $vectorSearch Semantic recall over events + Memory's historical-analog lookup.
2 Atlas Search ($search) Exact ticker / entity precision in the search bar.
3 Reciprocal Rank Fusion $facet-parallel vector + lexical hits fused at query time.
4 $graphLookup The 3-hop cascade walk over 1,149 supplier / customer / peer / sector edges.
5 $facet Parallel sub-pipelines for dashboard stats and hybrid search in one round trip.
6 Time-series collection Native OHLCV storage. The yfinance worker writes here.
7 TTL index 14-day expiry on events — keeps M0 under its 512 MB cap without a cron.
8 Change streams The push side of live updates — no polling, no webhooks.
9 Atlas Automated Embedding voyage-4 runs server-side on insert.
10 Voyage rerank-2.5 Cross-encoder reranks cascade candidates by narrative strength, not cosine distance.
11 voyage-multimodal-3.5 Chart and PDF embeddings for the multimodal endpoint.
12 MongoDB MCP server The agent society's tool layer. Every sub-agent talks to Atlas through MCP.
13 Performance Advisor Surfaced live in the UI — the system tells you which index would make it faster.

I started the project assuming I'd need a graph database, a vector store, and a search engine. Atlas does all three in a single aggregation pipeline. The architecture got smaller the more I leaned in.

⚠️ What almost broke

  • Three Atlas Search indexes, three jobs. M0 caps you at three. I needed ticker precision, semantic recall, and a hybrid scoring path. Fusing them at query time with $facet + RRF (instead of using the fourth index slot I didn't have) ended up being the right architecture anyway.
  • Tickerless events broke the model. A typhoon has no $NVDA. The whole click ticker → walk graph assumption falls over. The Coordinate Mapping System — Gemini 3 Pro JSON-mode, schema-validated regions and sectors, server-side lat/lon validation, globe arcs — was the hardest single feature to ship and the one I'm proudest of.
  • A 3D globe is a UX trap. Auto-orbit is delightful for four seconds and infuriating on second five when you're trying to click Taiwan. I added an interaction guard that suspends orbit the instant a user touches the canvas and resumes only after the cascade closes.
  • Cold start vs change streams. Cloud Run scales to zero, but a change stream needs a long-lived connection. The fix: a separate SSE service, a 15-second heartbeat to keep intermediaries from killing the connection, and a backfill query on every fresh client connect so the globe is never blank.
  • Five agents, one rate budget. Five Gemini 3 calls per cascade against a tight RPM cap meant the orchestrator had to fan out in parallel and tolerate any one specialist falling back to a local stub. That fallback path is what makes the demo reliable.

✍️ What I learned

  • Graph + vector + lexical in one query, on one cluster, is genuinely different. I assumed I'd need a graph DB, a vector store, and a search engine. Atlas collapses all three. Each capability you stop reaching for is a system you don't have to operate.
  • A society of small specialists beats one large generalist. Five focused Gemini 3 prompts in parallel produced a better cascade than one giant prompt that tried to do all five jobs. They were also individually fixable — when a prompt went off, I could replace one without retesting the others.
  • Treat every model output as untrusted input. Gemini 3 will occasionally hand you lat = 91.4. Pydantic at the boundary, server-side range checks, drop-and-log on failure. The Coordinate Mapping System works because I never trust it.
  • Graceful degradation matters more than clever orchestration. When Voyage throttles, the cascade still renders without rerank. When a specialist times out, the society shows a local fallback. The system never blocks on its weakest dependency.

🎯 What's next

The most interesting next step isn't a feature — it's a role switch. CascadeTerminal currently consumes the MongoDB MCP server. I want to ship an MCP server for CascadeTerminal, so Claude, ChatGPT, in-house copilots, and other agents can ask CascadeTerminal questions the same way CascadeTerminal asks Atlas questions today.

CascadeTerminal-as-infrastructure. That's the arc.


Solo build. Open source under Apache-2.0. Every URL in this submission is live.

Built With

  • aisstream
  • alpha-vantage
  • atlas-search
  • atlas-vector-search
  • change-streams
  • cloud-run
  • cloud-scheduler
  • docker
  • fastapi
  • finnhub
  • framer-motion
  • gdelt
  • gemini
  • gemini-3-flash
  • gemini-3-pro
  • google-adk
  • google-cloud
  • google-secret-manager
  • marketaux
  • mcp
  • mongodb
  • mongodb-atlas
  • motor
  • next.js
  • noaa
  • opensky
  • pydantic
  • python
  • react
  • react-globe.gl
  • reddit
  • rss
  • sec-edgar
  • shadcn-ui
  • sse
  • tailwindcss
  • three.js
  • typescript
  • usgs
  • vercel
  • vertex-ai
  • voyage-ai
  • voyage-multimodal-3.5
  • voyage-rerank-2.5
  • yfinance
  • zustand
Share this project:

Updates