Skip to content

Author

Ali Arbab

Project

01 / StockSaathi

Status

Live in production

Navigate

← projects

§ 01

Pitch

StockSaathi

An AI-coached paper-trading simulator that teaches Indian teenagers how markets — and their own reactions to them — actually work.

A virtual ₹1,00,000 portfolio priced off real NSE and BSE quotes: 4,324 equities, 351 ETFs and 8,800+ active mutual funds (as of 1 Oct 2026). Nine behavioural-bias detectors run on every trade, and an AI coach explains what just happened in the user's own numbers without ever giving a tip. Crash Replay re-runs real Indian market crashes day by day: three hand-built from real Nifty 50 closes, ten featured, and a generator for any other event. Placed 2nd nationally at the Masters' Union AI Buildathon; 170 accounts since, all by word of mouth.

Crash replay · COVID March 2020

Nifty 50 · Mar 2 – Apr 17, 2020 · ₹1,00,000 portfolio

HeldPanic-sold day 3

Held

₹83,249

-16.8%

Panic-sold day 3

₹1,01,063

+1.1%

Holding vs selling

₹17,814

Selling ahead by 21.4% so far

Pick a crash
D+3 of 15

§ 02

Access

§ 03

Traction

Built in 36 hours. Still growing, with no ads.

StockSaathi was built end to end at the Masters' Union AI Buildathon — a 36-hour national build — where a teammate pitched it to the panel and it placedsecond in India. It went live at stocksaathi.co.in the same month and has grown since entirely by word of mouth, with a student brand ambassador, Devansh, and no paid acquisition.

  • 170

    accounts created

  • 132

    have logged in

  • 100

    traded, queued an order or used the coach

  • 55

    made at least one trade

  • 79

    active in the last 30 days

  • 34

    active in the last 7 days

New accounts by month, 2026
AprMayJunJul52Aug84 so farSep
New StockSaathi accounts by month, 2026
MonthNew accounts
Apr 202619
May 20262
Jun 20269
Jul 20264
Aug 202652
Sep 202684 (in progress)

September counted to the 26th. No paid acquisition in any month.

As of . Test and throwaway-email sign-ups are included in the 170; excluding them leaves about 160.

§ 04

Stack

  • Vanilla ES modules, no build step
  • Python on Vercel (one ASGI entrypoint)
  • Supabase Postgres + RLS + RPCs
  • Gemini 3 Flash / 3.1 Pro on Vertex AI
  • GitHub Actions data + order jobs
  • Yahoo + Tickertape + AMFI feeds
  • Service Worker PWA
  • Kotlin + Jetpack Compose (Android port)

§ 05

Origin

Telling a model not to lie was only half the fix. Giving it the real data was the other half.

Problem

Indian high-school curricula teach the mechanics of money — compound interest, GST, simple budgeting — and skip behavioural finance entirely. Teenagers leave school with formulas but no instinct for how their own brain will sabotage them once real rupees are involved. The market education that exists is written for adults, in English, by full-time investors. It reads like job training, not learning.

Why me

I'm 17. I've watched the people around me get their first taste of investing through whatever influencer's reel showed up that morning. The advice ranges from technically wrong to actively harmful. I'm one peer group away from the audience that needs this — not a finance professional translating down, but someone who started where they're starting now.

Learned

My users can't tell a real number from an invented one, so a confident wrong answer is the worst thing the coach can do. I learned that by reading over a thousand logged coach messages: it had made up an entire trading record for one user and defended it for four turns, while every automated test I'd written stayed green. Telling a model not to lie was only half the fix. Giving it the real data was the other half.

More on Ali's journey & projects

/about →

§ 06

Architecture

One entrypoint, server-priced trades, all money in paise.

The front end is vanilla ES modules with no framework and no build step, cached by a service worker so it installs as an app. The Python API sits behind a single ASGI entrypoint: sixteen handlers dispatched from one function. That shape is a scar — Vercel's free tier allows twelve functions per deployment, the API had grown to twenty-one, and every deploy for four months quietly failed after a successful build while the site kept serving the last good one. Collapsing them into one entrypoint is what finally shipped those commits.

A full hot-mirror failover is built and tested — a Cloudflare Worker in front, Pages for static files, a Fly.io copy of the API, and a CI check that fails if a new endpoint isn't mirrored. It is not switched on yet: turning it on moves the whole serving path, so production still answers straight from Vercel until that cutover is planned properly.

Every mutating write goes through a SECURITY DEFINER PL/pgSQL function. The client can't write the money tables at all — the apply_trade RPC takes auth.uid() itself, takes a FOR UPDATE row lock, validates the trade, prices the trade from the server's own quote rather than the client's, applies cash + holdings + transactions transactionally, and returns a JSON envelope. Idempotency keys are UNIQUE(user_id, key) so a network-retried POST short-circuits and returns {ok:true, idempotent:true} instead of double-spending.

-- apply_trade locks the row, then conditionally updates only if cash suffices.
PERFORM 1 FROM public.portfolios WHERE user_id = v_user_id FOR UPDATE;
UPDATE public.portfolios
   SET cash_paise = cash_paise - v_value
 WHERE user_id = v_user_id AND cash_paise >= v_value;
IF NOT FOUND THEN RAISE EXCEPTION 'insufficient_cash'; END IF;

Money is stored in integer paise (bigint) end-to-end so portfolio totals never drift via floating-point. numeric(18,6) is used only for fractional MF shares. Instrument fundamentals come through a 4-tier merge (Tickertape ships first because its dividend yield + P/E (TTM) are self-consistent and fresher than Yahoo's consumer-page-derived numbers; Yahoo v10 + v7 fill gaps; Yahoo v8/chart is anonymous last-resort), with a +-joined provenance string written to fundamentals_cache.source so data lineage survives the cache row.

§ 07

AI coach

Deterministic where stakes are high, generative where they aren't.

The coach is built around two tracks that never trade jobs. Track 1 runs nine deterministic bias detectors against every BUY/SELL — panic-sell, FOMO, concentration, sector concentration, disposition effect (Shefrin & Statman 1985), anchoring, churning, pump-chase, overtrading. Each detector is a pure function returning {bias, severity, evidence} | null with explicit numeric thresholds tuned against real NSE volatility (panic-sell fires at ≥5% drop over 3 sessions OR ≥3% intraday, on a holding <21 days at >2% loss). The orchestrator picks a pre-written reflection template, attaches a historical analog (“In the last 12 dips of ≥10% on the Nifty, prices recovered to their prior high in a median of 22 trading days”), and a warning level. The LLM is optional flavour.

Track 2 is the conversational chat surface (“Saathi”) — a tool-use loop with eight function tools — portfolio, trade history, watchlist, pending orders, prices, search, news, crypto — executed in parallel via Promise.all, with tool responses capped at 4000 chars before re-feeding. The crypto tool bakes the warning into its own response (note: "India: crypto gains taxed at 30% + 1% TDS per trade since 2022") so the guardrail lands in the model's context regardless of whether the prompt remembers to ask for it.

The model behind it is Gemini on Vertex AI, proxied through /api/chat: Gemini 3 Flash for chat and tool calls, 3.1 Pro for reasoning, Flash-Lite for strict-JSON jobs. The biggest speed win wasn't architecture. A tool turn has two phases that want opposite things — deciding which tool to call, then writing the answer — and one reasoning setting was serving both. Dropping the thinking budget on the writing turn cut time to first word from about 6.6 seconds to about 1.4, measured on production. Each upstream attempt is bounded at 9 seconds, and a response header records every attempt so a degraded chain is visible instead of just slow.

Three independent layers keep it inside SEBI's lines — pre-LLM checks, in-prompt rules, and a post-LLM scanner for forbidden phrases — because a prompt rule is a preference, not a guarantee.

# SEBI-SAFE GUARDRAILS (ABSOLUTE)
- You can state a current price. That's public info.
- You CANNOT say: "should buy", "should sell", "recommend", "target price",
  "guaranteed", "sure shot", "will go up", "will crash".
- You CANNOT predict future prices, returns, or outcomes.
- If the user asks "should I buy/sell X?" → redirect to a reasoning framework
  (business health, valuation, drawdown tolerance, portfolio fit).
  Do not answer yes/no.

Conversations are stored per user in Postgres behind row-level security, so they follow the account across devices — and a shared family laptop never shows the last person's chats, a leak I found and closed in September. A user can also paste their own API key in Settings and route the coach through their own account.

§ 08

Time travel

The LLM picks the story. Yahoo data is the truth.

Pick (or invent) a market crisis; watch a ₹1,00,000 portfolio split into a held line and a panic-sold-on-day-3 line over real historical closes. Three hand-built replays (the COVID-19 crash, the 2008 crisis and demonetisation) use a real Nifty 50 close for every step, and ten featured replays are ready to run — Harshad Mehta in 1992, the dot-com crash, the 2008 crisis, Satyam, IL&FS, DHFL, YES Bank, COVID, the Paytm listing and Adani-Hindenburg — with educator-tone narration. Anything else routes through the custom-crash generator, a three-phase grounded pipeline:

  1. Phase A — pick dates and ticker. Gemini Flash with a colloquial-to-formal mapping table identifies any Indian market event from any phrasing: “the soap guy scam” → Nirav Modi / PNB, “demon” → demonetisation, “yes guy” → YES Bank moratorium, “the short seller thing” → Adani-Hindenburg.
  2. Phase B — fetch real historical data. No LLM. Yahoo via /api/ai?op=history. Primary symbol fires in parallel with up to three companion sector indices so Phase C can write relativity-aware narration.
  3. Phase C — narrate over real numbers. JSON output with title, key moments, recovery days. Every numeric field is then overwritten with the Yahoo-real value before persistence — the LLM's job is the story, the numbers are facts.
// Overwrite any hallucinated numbers with the REAL ones. The LLM's
// numbers are a sanity cross-check; the real-data numbers are truth.
meta.startIndex = Math.round(startIdx * 100) / 100;
meta.troughIndex = Math.round(troughIdx * 100) / 100;
meta.endIndex = Math.round(endIdx * 100) / 100;
meta.troughDay = troughDayIdx;
meta.indexDrop = Math.round(realDropPct * 10) / 10;

Scenarios are shareable across users by URL. The cache key is SHA-256 of an aggressively normalised prompt — split letter/digit runs, lowercase, non-alnum to spaces, tokenise, dedupe, sort, hash. So "Adani Hindenburg 2023", "hindenburg 2023 adani", and "AdaniHindenburg2023" collapse to one cache row, one shareable URL, one LLM cost amortised across every user who follows the link.

§ 09

Universe

Illustrative · NSEsample prices
RELIANCE1,247.30TCS4,089.50INFY1,856.75HDFCBANK1,634.20SBIN821.45WIPRO542.80

4,324 equities, 351 ETFs and 14,165 AMFI mutual fund schemes (about 8,800 still active), rebuilt every morning. AMFI India

The instrument universe is rebuilt daily by two pure-Node (zero deps) scripts running on GitHub Actions — NSE blocks requests from Vercel's servers, but a GitHub runner gets the same 2,568-row master list a home connection does. The equity build fetches the NSE master CSV, 17 NIFTY index constituent CSVs, and the NSE ETF API, warming a per-host cookie jar with browser-like Sec-Fetch-* headers because both NSE and NiftyIndices 403 anything else. A 27-value sector taxonomy is derived through a layered pipeline (NSE's industry tag → sectoral overlays → 100-line keyword regex for the long-tail ~1,500 small-caps). NIFTY index membership is packed into a 5-bit field per instrument, driving cap-bucket and risk-tier classification.

The MF build parses AMFI's proprietary semicolon-delimited NAVAll.txt (~17,000 raw rows, ~14,000 unique scheme codes), extracts AMC + category + plan + option from interleaved category headers, rolls 47 SEBI sub-categories into 7 buckets (Equity / Debt / Hybrid / Index / Solution / Commodity / FoF), and maps each fund to a benchmark. Output ships as content-addressed immutable JSON with a Brotli-q11 sidecar. A vercel.json rewrite swaps to .br when Accept-Encoding contains br.

Live quote caching is market-hours-aware: is_market_open_ist() drives both the Supabase TTL and the edge Cache-Control value — 5 seconds while NSE is open, 300 seconds while closed. Scheduled jobs refresh instruments, fund NAVs, fundamentals and a tiered quote warm-up; each self-bails before the function time limit and resumes from where it stopped on the next run.

§ 10

Polish

  • 800ms-debounced hottest-200 LRU cache

    The persisted-quote cache (ss.quotes.v3) writes only the 200 hottest symbols by last-access timestamp. A one-shot legacy migration deletes pre-split Reliance values from older keys so nobody paints with stale 2024-era numbers after a service-worker refresh.

  • Server-time anchor via performance.now()

    The market-status badge can't be spoofed by changing the system clock. The anchor is (performance.now() at sync midpoint, server's epoch ms); serverNow() adds the monotonic delta. Flipping your laptop to '9:30 AM on a Sunday' cannot fake 'Market Open'.

  • Intervention modal with 3-second read delay

    Before a panic-sell, the user sees historical recovery analog data. The 'Sell anyway' button counts down 3-2-1 before enabling. Escape and overlay-click are no-ops by design — Escape shakes the Hold button instead of closing.

  • Idempotency-key-based atomic apply_trade RPC

    transactions.idempotency_key is UNIQUE(user_id, key); a deterministic key generated once per click is held across retries. The PL/pgSQL function row-locks the portfolio, conditionally decrements cash, upserts holdings, and inserts the transaction in one transaction.

  • Chunked render with RAF yield

    The MF browser paints thousands of fund cards in 200-card chunks with requestAnimationFrame yields between batches; 'Tab not responding' never fires. Two cooperating IntersectionObservers (200% rootMargin to hydrate, 600% to dehydrate back to skeleton) keep the DOM bounded.

  • Orders fill without the app open

    Limit orders and AMOs used to execute only inside the user's browser tab — and for teenagers the market is open during the school day, so orders sat unfilled for up to 128 days. Matching now runs server-side on a schedule, priced from the server's own quotes; 74 stuck orders were filled at their original limit and 24 users got an apology notice.

§ 11

Hardening

September: the month I stopped trusting the client.

Reading the production database end to end turned up the kind of bugs a demo never shows. Orders only executed while the user's own browser tab was open — and for teenagers the market is open during the school day — so some sat for 128 days with cash reserved. Trade prices were taken from the client. And the money tables would accept a direct write from any logged-in user.

  • Server-side execution

    Limit orders and after-market orders are matched on a schedule by the server, priced from its own quotes. 74 stuck orders were filled at their original limit and 24 users got a personal apology notice.

  • Server-priced trades

    Every trade books the server's reference price; the client's number is advisory. Money tables accept no direct writes — only the trade functions can change a balance.

  • Red-teamed

    24 adversarial probes as a real logged-in user — minting cash, poisoning prices, writing another user's rows, calling admin functions. None got through, and CI tests now fail if any fix regresses.

The same week I rebuilt the coach from its logs. Most of its 46 defects were found by reading over a thousand real coach messages rather than by any test — including a trading record it had invented for one user and defended for four turns. The fix was two-sided: forbid invention in the prompt, and give the model real trade history, real orders and a real watchlist so it never has to guess.

§ 12

Android

A native Android client in Kotlin.

The web app stays the primary product, but a native client is on the way: a Jetpack Compose port with integer-paise money maths, all nine bias detectors ported to the same thresholds as the web, the panic-sell intervention, crash replay, limit orders, news, mutual funds and a dark mode — at version 0.5.0 with a signed release build. It talks to the same API as the website, so no provider key ever ships inside the app.

§ 13

Honest limits

  • Yahoo NSE data is officially 15-minute delayed. The “LIVE” badge flips to “DELAYED” via the staleAgeMinutes flag when Yahoo's own ts is older than 5 minutes during market hours. The product calls itself a paper-trading simulator, never a real-time tick feed.
  • The coach never gives buy/sell advice. The decision is structural, not stylistic — the prompt forbids it, the output filter blocks 25 forbidden phrases, the deterministic detectors carry the regulatorily sensitive output.
  • SELL-side limit orders don't reserve quantity. The schema comment is candid: “multi-order users can oversell. For the pitch scale this is acceptable.”
  • NSE 2026 holiday list is hardcoded. NSE doesn't expose a public holiday API; the file flags itself for annual update.
  • No real money. No real trades, no real advice — the legal posture that lets a teen-targeted app sidestep India's investor-suitability regulations.

§ 14

Numbers

  • 4,675

    NSE + BSE instruments

  • 8,800+

    active AMFI mutual funds

  • 9

    deterministic bias detectors

  • 8

    coach tools

  • 13

    crash replays (3 hand-built, 10 featured)

  • 3

    SEBI guardrail layers

  • ~1.4s

    coach first word (was ~6.6s)

  • 24

    red-team probes, 0 breaches

  • 5s / 300s

    market-open / closed TTL

  • 4-tier

    fundamentals fallback

  • 170

    accounts, no ads

  • paise

    all money as bigint

§ 15

Next

What happens next.

  • A school paper-trading competition run on StockSaathi, open to Classes IX–XII.
  • The Android client to full parity with the web, then onto the Play Store.
  • Going all in after Class XII boards — and, once I turn 18, building StockSaathi toward a real brokerage.
↑↓ navigate↵ selectESC close
15 results
StockSaathi — AI-coached paper trading for teens — Ali Arbab