← Back to work
02 AI Consultant ● Live in production

InvestoraAI

A stock intelligence platform built solo from scratch, designed around one weekly decision rather than a feed of noise.

LangGraph RAG FastAPI React n8n Pinecone SSE Streaming TypeScript
InvestoraAI: landing page
Type Personal project · Full-stack AI product
Role Solo build: product, pipeline, frontend, backend, deployment
Year 2026

Data-rich, action-poor.

A retail investor with a watchlist, a job, and an opinion doesn't have an information problem. They have a prioritisation problem. The signals exist: prices, fundamentals, earnings surprises, volume spikes, but they're buried under feeds designed to maximise attention, not inform decisions. Twitter shouts. Bloomberg drowns. Most apps flatten everything to momentum cards with no connection to what you actually hold.

Most AI tools in this space fail in one of three ways: they generate plausible-sounding analysis without a clear action, they claim personalisation while delivering the same output for every user, or they predict rather than prioritise. Impressive in demos. Unreliable in practice.

For the investor

Sunday evening. Three things from your watchlist worth thinking about this week. One or two opportunities you wouldn't have found yourself. A flag if something you hold has drifted away from the profile you gave the system.

For the build

Prove that starting with the decision, not the model's capabilities, produces a better AI product. Every architectural choice follows from that.


Four decisions that shaped the whole product.

01

Define the decision first, not the pipeline

The first question was not what the AI should do, but what decision it should support. The answer: a weekly prioritisation call on which opportunities deserve attention and why. That single framing drove every subsequent choice: weekly cadence, conviction score over prediction, structured dashboard over chat interface.

02

Two delivery channels, one principle: evaluated during runs

The weekly digest and the live alert system look like different decisions but follow the same rule: evaluation happens during a scheduled analysis run, not by a continuous price-watching daemon. Alerts are checked against fresh data already fetched in that run. "Live" means evaluated on real data, not polled every tick, same signal quality, no background infrastructure cost or added complexity.

03

Conviction score over prediction

The system produces a composite score from two dimensions: quality from fundamentals (profitability, balance-sheet health, revenue and earnings growth) and momentum from the 5-day price return, both normalised and blended at 55/45. Every signal carries an action frame: Review, Monitor, or Trim Risk. The weights live in environment variables, not in a prompt, a deliberate preference for medium-horizon signals over swing-trade noise.

04

Streaming transparency as a product feature

The analysis pipeline emits live progress events to the frontend via SSE so users see the agent working in real time: data collection, scoring, synthesis, delivery. Transparency about process isn't an implementation detail; it's the mechanism that makes the output feel trustworthy rather than a black box.

What I cut

A chat interface: wrong shape for a prioritisation decision. A continuous price-monitoring daemon: alerts are evaluated during analysis runs against fresh data instead, which gives the same signal quality with no background infrastructure cost. Multi-user and advisor mode: sequenced for v2. Portfolio-level P&L tracking: the product is about signals, not portfolio accounting. Each cut had a reason; none was accidental.

What I refused to claim

Investment performance. The system surfaces signals and supports decisions; it does not make them. Every AI View includes an explicit disclaimer. The product is not a robo-advisor and is designed so it cannot be mistaken for one.

Built first
  • LangGraph ReAct agent pipeline
  • Composite conviction scoring
  • SSE streaming progress UI
  • Dashboard + stock analysis views
  • Watchlist with live quotes
  • Profile-aware signal filtering
  • Quality × momentum strategy map
  • Weekly email digest (n8n · HTML template)
  • Telegram alerts: high-urgency signals + user-defined price thresholds
Cut
  • Chat interface
  • Continuous tick monitoring daemon
  • Multi-user / advisor mode
  • Portfolio P&L tracking
  • Mobile app
Deferred
  • Decision quality tracking
  • Multi-user architecture
  • Portfolio scenario views
  • Additional data providers
  • Fine-tuned scoring weights

17 stages. Two LLM calls. Everything else is deterministic code.

The first design decision was what the AI should and shouldn't do. The pipeline has 17 nodes. The LLM is called in exactly two of them: once to pick the next data action during collection, once to frame the evidence per ticker in plain language. Everything else (scoring, anomaly detection, personalisation, alert checking, persistence, delivery) is deterministic Python. That boundary wasn't accidental. It made the pipeline testable, the costs predictable, and the output debuggable: when a signal is wrong, you know which node produced it.

Scoring is transparent by design. Quality comes from three fundamentals dimensions: profitability (return on equity, operating margin), balance-sheet health (debt-to-equity), and growth (revenue and earnings growth), each normalised to a 0–10 range and blended with fixed weights. Momentum is the 5-day closing price return, normalised across the ticker universe. The two combine at 55/45 in favour of quality, a deliberate preference for medium-horizon signals over swing-trade noise. The weights live in environment variables, not in a prompt.

Personalisation operates on three axes: whether the ticker is on the user's watchlist (binary, always highest priority), how well the signal fits their stated profile (sector interests, asset class preferences, investment horizon), and whether it clashes with their risk tolerance. The third axis is what drives mismatch alerts: a high-severity signal for a low-risk user surfaces explicitly rather than quietly disappearing. No user asks for this. It's the most opinionated feature in the product, and the one most likely to be genuinely useful.

Two notification families ship from the same pipeline run. The weekly digest is gated: it fires only when the run is full-scope and the user has opted in with a confirmed email. When the gate passes, a webhook triggers an n8n workflow that renders a formatted HTML email with that week's ranked candidates. Telegram alerts are different: they fire during every eligible run and cover two independent subtypes. High-urgency personalised signals (severity flagged as High by the personalisation layer) bypass the weekly cadence and post immediately. User-defined price alerts check explicit thresholds the user configured (price above, price below, daily move beyond a percentage) against the fresh quote data already fetched in that run. Both types share one design principle: delivery is soft-fail. If the Telegram webhook errors or the email workflow fails, the exception is caught and appended to the run's error log: the analysis still persists to the database and the dashboard still updates. Pipeline success is never coupled to notification delivery.

DATA IN PIPELINE DELIVERY Market prices yfinance · Marketstack Fundamentals Financial Modeling Prep News Finnhub Past signals Pinecone · RAG Collect Fetch & validate all provider data Score Quality 55% Momentum 45% ★ LLM CALL Synthesise Evidence → plain-language narrative per ticker Personalise Watchlist · profile fit risk mismatch USER PROFILE Risk · horizon interests · constraints Dashboard React · SSE streaming Telegram High-urgency + price alerts Email digest Weekly · Monday 15 of 17 nodes are deterministic Python: scoring, ranking, anomaly detection, mismatch alerts, persistence. The LLM frames evidence into plain language. Cost and latency stay predictable.

Fully deployed and live on real market data.

0→1 Solo build: product, pipeline, frontend, backend, deployment
3 Market data providers integrated with failover handling
Live Real market data, real pipeline, real deployment on Vercel + Render

Shipped & live

  • End-to-end pipeline running on real market data: data collection, scoring, synthesis, streaming delivery
  • Full React/TypeScript dashboard with SSE progress streaming, run history, per-ticker analysis, strategy map, watchlist, and profile settings
  • Conviction scoring model with explicit quality/momentum weights and three-band signal classification
  • Profile-aware filtering : risk tolerance, investment horizon, and asset class preferences shape what surfaces for each user
  • Weekly email digest gated on run scope and user opt-in , rendered by an n8n workflow and delivered via HTML email
  • Telegram alerts covering two subtypes: high-urgency personalised signals and user-defined price thresholds (above, below, daily move)
  • Soft-fail delivery : notification failures are caught and logged; pipeline persistence and dashboard updates are never blocked
  • Mock mode with deterministic fixtures for regression testing without provider cost or variance
  • Deployed and accessible at investora-ai.elsabakiu.com

Deliberately not claimed

  • Not claiming investment performance. The system supports decisions; it doesn't make them. No backtest, no return attribution, no accuracy headline.
  • Not claiming user validation. This is a solo build and a working product, not a validated product with measured retention or decision-quality data yet.
  • Not claiming scoring model optimality. The quality/momentum weights are explicit product decisions, not the result of optimisation against real outcomes. They need real usage data to calibrate.
  • Multi-user is architecturally ready but not shipped. The system is designed for it; it hasn't been built.

What I'd do differently. What changed my mind. What transfers.

i.

Define the scoring model earlier. I refined the conviction bands and weights iteratively through the build. Defining them upfront as explicit product decisions, with documented rationale, would have made every pipeline and UI decision faster. In an AI product, the scoring logic is the product spec. Treat it like one from day one.

ii.

SSE streaming changed how the product feels, not just how it performs. I added live progress streaming because the pipeline takes time and a blank screen erodes trust. What I didn't expect: showing the agent's work step by step made the output feel more credible, not just less frustrating. Transparency about process is a trust mechanic. I'd design for it from the start in any AI product with latency.

iii.

The decision-first framing is the most transferable thing here. Before writing a single prompt, I asked: what specific decision does this product support, and what does the user need to make it well? That question ruled out a chat interface, a prediction engine, and a data dashboard, all before any code. The pattern works for any AI product where the output is a recommendation rather than a generation: clinical notes, investment signals, supply chain prioritisation, hiring screens. Define the decision. Shape the product around it.

iv.

Schema discipline matters more than prompt discipline. Typed contracts: Pydantic on the backend, Zod on the frontend, the same shape on both sides: did more for output quality than any prompt revision. Once the output shape was guaranteed, the UI became deterministic, tests became diffable, and prompts became swappable. On any AI product with structured output, I'd establish this contract before writing the first prompt.

v.

Hard constraints belong at the product layer, not in the prompt. Rules like "no crypto," "ESG only," or "maximum 20% position" are enforced in deterministic code before any LLM call. Prompt-layer constraints are soft suggestions; product-layer constraints are guarantees, which is what the user actually signed up for. The same logic applies to mismatch alerts: the risk check is code, not a request to the model.

vi.

Treat notification delivery as an independent concern, never a dependency. Coupling pipeline success to delivery success creates a fragile system where a Telegram API timeout can kill a user's weekly analysis run. Catching every delivery exception, appending it to the run's error log, and letting the dashboard update regardless means the core product value (the analysis) is always delivered. The notification layer is additive. On any system where a background job serves both a core function and a delivery side-effect, I'd establish that boundary explicitly in the first sprint.