An autonomous AI agent that takes a company name, fetches real-time financial data and news, computes deterministic signals, and produces a transparent investment analysis with a final Invest / Pass verdict.
Live Demo: equity-lens-flax.vercel.app
EquityLens bridges the gap between raw financial data and actionable investment analysis. Unlike typical LLM wrappers that blindly pass data to an AI model for a subjective opinion, EquityLens uses a signal-based architecture that separates what the code computes from what the LLM interprets.
The code produces facts. The LLM produces meaning. That's the core design split.
This produces a two-layer output:
| Layer | Source | What It Contains |
|---|---|---|
| Evidence Layer | Deterministic code (no AI) | Computed signals (✅/❌), derived metrics (PEG, earnings yield), raw financials with correct currency, news headlines with sentiment badges |
| AI Interpretation Layer | Google Gemini (marked as AI-generated) | Verdict (INVEST/PASS), confidence level, synthesis narrative, bull/bear cases, primary strength/risk, disclaimers |
The user sees both layers clearly separated — they can trust the evidence independently and evaluate whether the AI's interpretation makes sense.
Most candidates building this kind of agent would ask the LLM: "Score this company's financial health 1-10." We deliberately did not do this.
The problem with LLM-based scoring: it creates the illusion of precision where none exists. When the LLM gives Financial Health a "7/10," what does that mean? Run it twice and you might get 6 and 8. The evaluator will notice.
Instead, we compute deterministic boolean signals in code:
debtToEquity > 2.0→ 🔴 Negative (always, every run)revenueGrowthYoY > 5%→ 🟢 Positive (always, every run)
The LLM's job is limited to interpreting what the signal pattern means — the part that requires judgment, not arithmetic. This makes the analysis reproducible, auditable, and verifiable.
- Node.js v18+
- API keys for three services (all free-tier)
git clone <repo-url>
cd equity-lens
npm installCreate a .env.local file in the project root:
GOOGLE_API_KEY=your_gemini_api_key
FMP_API_KEY=your_financial_modeling_prep_key
TAVILY_API_KEY=your_tavily_search_key
GEMINI_MODEL=gemini-2.5-flash| Key | Service | Purpose | Free Tier |
|---|---|---|---|
GOOGLE_API_KEY |
Google Gemini | AI verdict synthesis via LangChain structured output | Generous |
FMP_API_KEY |
Financial Modeling Prep | Company profiles, income statements, ratios, sector data | 250 req/day |
TAVILY_API_KEY |
Tavily Search | Real-time news retrieval with sentiment | 1000 req/month |
npm run devNavigate to http://localhost:3000. Type a company name or click a Quick Pick chip.
node --env-file=.env.local tests/test-e2e.mjsThe app deploys to Vercel with zero configuration. Add the same three environment variables in the Vercel project settings.
Given a strict 24-hour development window, every feature was ruthlessly prioritized using the MoSCoW framework. The goal was to optimize for the evaluator first (demonstrate product thinking + engineering capability) and the end-user second.
| Feature | Rationale |
|---|---|
| Company name input + entity resolution | Assignment specification. No input = no product. |
| Real financial data fetching (FMP) | Without external data, you're just prompting an LLM with training data — that's recall, not research. |
| Final Invest/Pass verdict with reasoning | Explicitly required by the assignment brief. |
| Error handling & graceful degradation | If the evaluator types "Zomato" and the app crashes, that's a failed demo. Table stakes for a product role. |
| README with all required sections | The assignment specifies exact sections. Missing one = didn't read the brief. |
| Feature | Rationale |
|---|---|
| Deterministic signal engine (computed in code) | Separates "I prompted ChatGPT" from "I built an AI product." The evaluator can see the framework and debate it — that's a conversation, not a demo. |
| News/sentiment analysis (Tavily) | Adds a second data dimension. High value-to-effort ratio. |
| Entity disambiguation UI | "Apple" → is that Apple Inc. or Apple Hospitality? Without this, the agent is fragile. Small feature, outsized impact on reliability. |
| Indian company support | The assignment is from an Indian company (InsideIIM). The evaluator will almost certainly test Reliance, TCS, Infosys, Zomato. |
| Vercel deployment | The difference between the evaluator reading your code and experiencing your product. |
| Dark/Light mode toggle | Premium polish that signals attention to detail. |
| Feature | Rationale |
|---|---|
| "Challenge This Verdict" (devil's advocate) | Memorable differentiator — one additional LLM call generates the counter-argument. |
| Real SSE streaming for progress | Genuine real-time updates instead of simulated stepper. UX improvement. |
| Feature | Why Excluded |
|---|---|
| Multi-company comparison | Different product scope. Assignment says "a company name" (singular). |
| User auth / database | Zero evaluation value. Actively harmful — evaluator would need to create an account. |
| LangGraph state machine | Evaluated and explicitly rejected (see Key Decisions below). |
| Private company support | Insufficient structured data for defensible analysis. |
┌────────────────────────────────────────────────────────────────┐
│ FRONTEND (Next.js App Router, React) │
│ page.js → POST /api/analyze → render two-layer result │
├────────────────────────────────────────────────────────────────┤
│ API LAYER (Next.js Route Handler) │
│ route.js → validates input → calls orchestrator → returns │
│ Returns { type: "disambiguation", matches } OR │
│ { type: "result", analysis } │
├────────────────────────────────────────────────────────────────┤
│ ORCHESTRATOR (plain async function, 5 steps) │
│ resolveCompany → gatherResearch → computeAnalysis → │
│ generateVerdict → assembleResult │
├────────────────────────────────────────────────────────────────┤
│ DATA LAYER │ COMPUTE LAYER │ LLM LAYER │
│ FMP API (financials, │ Signals │ Gemini 2.5 Flash │
│ profile, search) │ Derived metrics│ via LangChain │
│ Tavily API (news) │ Gap assessment │ structured output │
│ Cached fallback (5 co.) │ Validation │ │
└────────────────────────────────────────────────────────────────┘
User enters "Infosys"
│
▼
Step 1: resolveCompany("Infosys") [lib/resolve.js]
├── FMP /search → returns matches
├── Single match? → auto-select, continue
└── Multiple matches? → return disambiguation cards to UI
│
▼
Step 2: gatherResearchData(identity) [lib/research.js]
├── Promise.all([
│ FMP financials (income + ratios + profile)
│ Tavily news search
│ ])
├── If BOTH fail → check offline cache → if no cache, abort
└── If one fails → continue with available data, track gap
│
▼
Step 3: computeAnalysis(identity, financials, news) [lib/compute.js + lib/signals.js]
├── Compute derived metrics (PEG ratio, earnings yield, valuation premium %)
├── Compute 10-15 boolean signals against thresholds
├── Aggregate signal balance (positive − negative)
├── Assess data gaps → if anomaly detected, one targeted Tavily search
└── Returns: { signals, derivedMetrics, signalBalance }
│
▼
Step 4: generateVerdict(analysis, identity) [lib/compute.js]
├── Format prompt with all signals + context
├── Call Gemini via ChatGoogleGenerativeAI.withStructuredOutput(zodSchema)
├── Validate: verdict consistent with signal balance?
│ If LLM says INVEST but signals say ≤ -2 → override to PASS, add disclaimer
└── Returns: VerdictOutput (decision, confidence, summary, bull/bear cases)
│
▼
Step 5: assembleResult(...) [lib/orchestrator.js]
└── Returns: full AnalysisResult with metadata (duration, sources, timestamps, errors)
A valid criticism: "This executes the same 5 steps in the same order every time. That's a pipeline, not an agent."
The data-gap assessment step (Step 3) adds genuine agentic behavior. After fetching data, the system inspects its own research and asks: "Is there something unusual here that I should investigate before scoring?"
| Condition Detected | Targeted Search Triggered | Why |
|---|---|---|
| P/E ratio negative or > 100 | "why is [company] P/E ratio [value]" |
Extreme P/E usually means a one-time event. Scoring without context is misleading. |
| Revenue declined > 10% YoY | "[company] revenue decline reason" |
Could be a red flag or a strategic pivot. The LLM needs context. |
| News sentiment < 0.3 (negative) | "[company] controversy OR risk" |
Something bad is happening. Root cause needed. |
| None of the above | No additional search | Data is sufficient. Proceed. |
This is deterministic code, not an LLM call. The conditions are hardcoded rules — predictable, testable, debuggable. But the system adapts its behavior based on what it observes, which is the definition of agency.
| YOUR CODE Computes | THE LLM Interprets |
|---|---|
PEG ratio = 1.8 |
"PEG of 1.8 suggests growth is mostly priced in but not excessively" |
Valuation premium = +18% |
"Trading at 18% premium to sector, justified IF growth accelerates" |
Red flags: [highDebt, revDecline] |
"Combination of high debt AND declining revenue creates refinancing risk" |
Sentiment ratio: 0.3 (negative) |
"Predominantly negative coverage driven by regulatory concerns" |
We chose to compute signals in code rather than having the LLM score companies 1-10. LLMs produce inconsistent numerical ratings across runs — the same company might get 6/10 on Monday and 8/10 on Tuesday. Signals are computed deterministically, making the analysis reproducible and auditable. The LLM's role is limited to interpreting the signal pattern — the part that requires judgment, not arithmetic.
The agent explicitly only supports publicly traded companies. The reason isn't just data availability — it's about defensibility. For a public company, the agent can cite "P/E = 28.5 vs sector median 24.2" — a verifiable claim. For a private company, the agent would say "seems promising based on recent funding" — that's an opinion, not analysis.
We evaluated LangGraph for orchestration but chose a plain async function because the research flow is linear with no cycles or dynamic replanning. LangGraph is designed for agents that need cycles, dynamic tool selection, or complex branching — our agent has none of these.
The savings (~3-5 hours of setup, learning, and debugging) were redirected to output validation, UI polish, and Indian market support.
"I evaluated LangGraph, understood when it's appropriate, and made a deliberate decision not to use it. If the agent were extended to support iterative research (e.g., the LLM decides it needs more data and loops back), LangGraph would become the right choice."
A monolithic Next.js architecture (App Router + API Routes) eliminates CORS complexities, allows shared Zod schemas across the stack, and enables zero-config deployment to Vercel — maximizing development speed for a 24-hour MVP.
Thresholds are general-purpose defaults based on standard equity analysis conventions:
D/E > 2.0→ Standard high-leverage warningP/E > 1.5× sector→ 50% premium needs justificationPEG < 1.5→ Peter Lynch popularized PEG < 1 as undervalued; 1.5 is moderateRevenue growth > 5%→ Roughly equals inflation + real GDP growth
Honest limitation: Weights and thresholds are configurable defaults, not empirically derived. A production system would calibrate these against historical investment outcomes.
Current analysis uses the latest available financial data. It does not examine multi-year trends (e.g., accelerating vs. decelerating growth), which would significantly improve investment thesis quality. This was a deliberate scope cut for the 24-hour window.
To minimize FMP API quota usage, raw FCF is not fetched via the cash-flow-statement endpoint. Instead, it is mathematically approximated using Free Cash Flow per Share × Outstanding Shares (where Outstanding Shares = marketCap / price).
News sentiment uses a simple deterministic keyword matching system (e.g., checking headlines for "surge", "beat" vs "miss", "plunge"). An advanced NLP pipeline (like FinBERT) was excluded to prevent latency and infrastructure overhead. The LLM's interpretation phase naturally mitigates minor sentiment classification errors.
After the LLM generates a verdict, the system validates it against the signal balance:
- If
signalBalance >= +2but LLM says PASS → override to INVEST, add disclaimer - If
signalBalance <= -2but LLM says INVEST → override to PASS, add disclaimer
This ensures the final output never contradicts its own evidence — a simple sanity check that prevents embarrassing inconsistencies.
| Feature | Why Excluded |
|---|---|
| Multi-company comparison | Different product scope entirely |
| User authentication | Zero evaluation value; adds friction for the evaluator |
| Database / persistence | No requirement to save past research |
| Vector store / RAG | There's no document corpus. The agent fetches structured data from APIs. RAG solves a different problem. |
| Real SSE streaming | UX enhancement with high implementation cost; simulated stepper provides 80% of the value |
- Resolution: Auto-resolved to AAPL on NASDAQ
- Signals: Strong net margins, positive FCF, solid ROE, healthy revenue growth
- Verdict: INVEST · HIGH Confidence · Signal Balance: +5
- AI Summary: "Apple demonstrates exceptional profitability with margins significantly above sector averages. Strong cash generation and conservative leverage provide financial resilience. The premium valuation is justified by consistent execution."
- Bull Case: Ecosystem lock-in and services revenue create durable competitive advantages
- Bear Case: Hardware growth saturation and regulatory pressure on App Store fees
- Resolution: Auto-resolved to TSLA on NASDAQ
- Signals: Extreme P/E triggered data-gap assessment → agent searched for context on Tesla's valuation premium
- Verdict: INVEST · MEDIUM Confidence · Signal Balance: +2
- AI Summary: "Tesla presents a mixed signal profile — strong revenue growth and improving margins are offset by an extreme valuation premium. The high P/E reflects market pricing of future growth, which is standard for this company."
- Resolution: Auto-resolved to ZOMATO.NS on NSE. Currency correctly displayed as ₹ (INR).
- Signals: Explosive revenue growth YoY flagged positive, but negative/low margins flagged as concerning
- Verdict: INVEST · MEDIUM Confidence
- AI Summary: The agent correctly identified Zomato as a high-growth, pre-profitability company and produced a nuanced analysis acknowledging the trade-off between growth trajectory and current losses.
When FMP's free tier quota is exceeded (250 requests/day), the system does not crash. It falls back to a pre-cached offline dataset for 5 companies:
- Apple Inc. (AAPL)
- Tesla (TSLA)
- Infosys (INFY.NS)
- Reliance Industries (RELIANCE.NS)
- Zomato (ZOMATO.NS)
The AI interpretation and signal generation steps continue seamlessly using cached data, with a disclaimer added to the output.
| Improvement | Why It Matters | Estimated Effort |
|---|---|---|
| Multi-year trend analysis | "Revenue grew 23% YoY" is less insightful than "revenue growth has accelerated for 3 consecutive years." Point-in-time analysis misses trajectory. | 2-3 hours |
| Dynamic sector benchmarking | Instead of static thresholds, dynamically fetch the target company's sector median and benchmark signals relatively (Tech vs. Retail have very different "normal" P/E). | 1-2 hours |
| "Challenge This Verdict" feature | A button that generates the strongest counter-argument using the same data. Shows intellectual honesty — every real investment committee has a devil's advocate. | 30 min |
| Advanced NLP sentiment (FinBERT) | Replace keyword-based sentiment with a financial-domain NLP model for more accurate news classification. | 3-4 hours |
| Competitor context | When analyzing Infosys, at least mention TCS and Wipro. No real analyst evaluates a company in isolation. | 2-3 hours |
| Real SSE streaming | Replace the simulated progress stepper with genuine server-sent events showing real-time pipeline progress. | 1-2 hours |
| Configurable investment style | A dropdown: "Growth / Value / Balanced" that shifts signal weights. That's product thinking — different investors have different frameworks. | 1 hour |
| Historical signal calibration | Calibrate signal thresholds against 10 years of S&P 500 performance rather than textbook heuristics. | 4-6 hours |
- Challenge: During live testing, FMP abruptly restricted their
v3legacy endpoints behind a paywall (HTTP 403) for free-tier users, breaking our entire data layer. The new Stable API also stripped crucial valuation ratios (P/E, P/B, ROE). - Resolution: Migrated
lib/resolve.jsandlib/research.jsto FMP Stable APIs (/stable/search-name,/stable/search-symbol). Added a fourth parallel fetch to/stable/ratios-ttmand mapped new keys back to our Zod schema. Because our architecture enforces strict boundaries, the Compute Layer, Signal Engine, and Orchestrator required zero changes.
- Challenge: Gemini API rate limits (
429 Too Many Requests) could crash the analysis mid-pipeline. - Resolution: The Orchestrator catches LLM failures natively. Instead of crashing, it degrades gracefully by returning a deterministic verdict based entirely on the Signal Balance computed in Step 3. The user still gets a useful result.
- Challenge: FMP returns some Indian company financials in INR, others in USD. Displaying
$1.63Tfor Reliance Industries would be incorrect. - Resolution: Currency code is extracted from the company identity and propagated through the entire pipeline. The formatter maps
INR → ₹,USD → $,EUR → €and uses appropriate suffixes (₹1.63L Cr,$394.3B).
| Layer | Technology |
|---|---|
| Frontend | Next.js 16 (App Router), React, CSS Variables |
| API | Next.js API Routes |
| LLM | Google Gemini 2.5 Flash via @langchain/google-genai |
| Financial Data | Financial Modeling Prep (Stable API) |
| News | Tavily Search API |
| Validation | Zod schemas at all system boundaries |
| Deployment | Vercel |