Case study
OctaK Edge
A research tool for Indian stocks: code computes every rating, and AI only explains it.
- Role
- Architecture and implementation, two-person team
- Year
- 2026
- Status
- In development

At a glance
- Indian stocks covered
- ~300
- holding timeframes
- 5
- component scores
- 7
- deployed apps
- 3
Overview
OctaK Edge is a research tool for people who trade or invest in Indian stocks. You look up a company and it returns a rating out of 100, the evidence behind that rating, and a trade plan with an entry price, a stop-loss and targets - or a refusal, when the setup is too weak to act on.
One rule governs the system: numbers come from code, words come from AI. Every score and price level is computed in TypeScript on the server, and the model only explains a verdict it did not produce.
The product concept and the scoring model are my collaborator's. I built the platform around them - the data layer, the analysis pipeline, the database, the access rules, the scheduled jobs and the three-app split. Two of us, pre-launch.
Why I Built It
Most retail stock tools publish a number with no working shown. Ask a chatbot instead and it improvises a confident recommendation. Neither can be checked, which is the one thing that matters when the answer costs money.
OctaK Edge takes the harder position: every number traces to a deterministic module, and the model may only explain values it did not compute. Writing that sentence is easy. Holding it true across free data sources, a serverless time budget, five timeframes and a second app on the same database is the engineering.
The Approach
My scope was everything between the scoring model and a product someone can use: fetching market data, running it through the engine, storing what it produced, deciding who sees what, and keeping it fresh on a schedule.
It shipped as a single Next.js app first - one deploy, and no service boundary to maintain while the surface was still moving. Once it stabilized I reviewed the whole codebase, wrote up twelve architecture decision records, and ran the split into three apps as a phased program rather than a rewrite.
Scope choices, recorded rather than rediscovered:
- No monorepo, and no separate repo for the schema or the shared package.
- No ORM - a typed client and per-table modules suit a dozen tables.
- No HTTP between apps, so no service mesh and no cross-app coupling.
- No API versioning with one first-party client - the seam is kept, not built.
Architecture
One Next.js app, no separate backend. A single per-symbol pipeline is the spine: parallel fetch across independent sources, indicators, pattern detection, then a deterministic chain - component scores, timeframe-weighted blend, confluence, recommendation, trade setup, projection. Both the analysis and position routes call it, so the two surfaces cannot diverge.
Every data adapter is wrapped in a fail-safe cache. A thrown error becomes a typed "unavailable" for that field, and the response lists what was missing. No adapter ever substitutes a default.
Screening ~300 names per request is too heavy, so a cron precomputes a universe snapshot and requests only read it; its timestamp anchors determinism. Picks are written to an append-only forward log before the market resolves them, each stamped with the engine version and a hash of the exact scoring code.
Today it runs as three apps over one Supabase project - a credential-free landing site, the product, and an operator console - sharing only the schema and one published package.
Decisions & Tradeoffs
Enforce the determinism boundary structurally, not with a system prompt
Tradeoff accepted
Every new signal must be modelled by hand, and the product cannot answer what the engine cannot compute. In return the model has no code path that writes a number - it explains computed values, and its pattern guesses are validated against a closed list at capped confidence.
Alternatives considered (3)
- Instruct the model not to invent numbers and trust it
- Let the model adjust a computed score when it is confident
- Generate the verdict with the model and compute a score afterwards to justify it
One shared database is the only contract between apps; no app-to-app HTTP
Tradeoff accepted
The schema becomes a coupling point two apps can break, so migrations are expand-contract, forward-only, and owned by one repo with a CI drift check. What it buys is zero runtime coupling - no internal auth, no availability dependency between apps.
Alternatives considered (3)
- The product exposes internal endpoints the console calls
- A second Supabase project for the console, with replication
- A shared service layer in front of both
Three repos, one published package; duplicate everything else on purpose
Tradeoff accepted
Contract changes are two-step and some code is maintained twice. The rule that makes it bearable: promote to the shared package only when drift would be a security or billing bug - everything else is duplicated on purpose, with a comment naming its sibling.
Alternatives considered (3)
- A monorepo with shared workspaces
- A fourth repo for the schema, or for the shared package
- Git submodules for shared code
Precompute the screening universe on a schedule; requests only read it
Tradeoff accepted
Results are only as fresh as the last snapshot, so every response carries its timestamp and a flagged live-fallback path. The worker tier stays designed-for, not built - the cut line is documented with its trigger conditions.
Alternatives considered (3)
- Scan the universe on request
- A job queue with workers
- A long-lived worker outside the serverless runtime
Build the whole data layer on free, non-key-gated sources
Tradeoff accepted
Sources can block or change without notice, so the adapter layer contains the blast radius and a blocked source degrades to a visible gap. The licensing question is commercial, carried as a business gate before the product charges anyone.
Alternatives considered (2)
- A paid market-data vendor from the start
- Official exchange feeds
Engineering Challenges
Scanning ~300 stocks on demand costs more time than a single web request has.
How it was solved
A cron builds the feature vector for the whole universe and caches it; a request reads that snapshot, filters by per-timeframe eligibility, and runs full analysis only on a handful of finalists, batched ten at a time. The snapshot timestamp anchors determinism so two requests in the same window agree.
Free data sources fail without warning, and the product still has to answer honestly.
How it was solved
A component with no inputs is dropped from the blend, not scored zero, so weights renormalize and confidence reflects real coverage. The trade-setup generator refuses with a reason when the setup is weak, and the one value that could be faked - historical accuracy - is hardcoded null and flagged as the highest fabrication risk in the file.
Splitting one app into three, without ending up with three apps that can only ship together.
How it was solved
The trap is a distributed monolith - three deploys that still have to move together. The schema got an owner (the repo with write-gravity), a single writer per table, expand-contract forward-only migrations, and a CI drift check. Rolling back app code is always safe against the newest schema, so schema rollback stops existing as a concept.
Features
- Deterministic Edge Score: seven component scores reweighted across five holding timeframes
- Trade plan with entry, stop, targets and reward-to-risk - or an explicit refusal when the setup is weak
- My Position: the same analysis adapted to a real entry price and quantity
- Edge Insight of the Day: a timeframe-specific screen over ~300 names, precomputed on a cron
- Append-only forward log: every pick recorded before the market resolves it
Tech Stack
- Next.js 16 (App Router)
- React 19
- TypeScript
- Tailwind CSS 4
- shadcn/ui
- Supabase (Postgres, Auth, RLS)
- Vercel + Vercel Cron
- Upstash Redis
- Zod
- TradingView Lightweight Charts
- yahoo-finance2
- technicalindicators
- Gemini / Anthropic / OpenAI (pluggable)
- Vitest
- Sentry
Lessons Learned
- A constraint that lives in a prompt is a suggestion. The determinism boundary held because the model was never given a code path that could write a number.
- Honest degradation costs less and is worth more than it looks. "Unavailable" beats a plausible default and deletes a class of bug nobody can reproduce.
- Deferring debt with an explicit revisit trigger turns rot into a decision.
Screenshots



Contact
If you need an engineer who can own it end to end, from the interface to the infrastructure, let's talk.
Email me