Case study
OctaK Edge
Evidence-based stock verdicts for Indian equities, where AI explains but never scores.
- Role
- Architecture and implementation, two-person team
- Year
- 2026
- Status
- In development

At a glance
- stock universe
- ~300
- holding timeframes
- 5
- component scores
- 7
- deployed apps
- 3
Overview
OctaK Edge turns an Indian stock into a structured verdict: component scores blended per holding timeframe, a trade plan with entry, stop and targets, and a position view that adapts to a real entry price and quantity.
One rule governs the system: numbers come from code, words come from AI. Every score, band and target is computed deterministically in TypeScript; the model only explains a verdict it did not produce, and proposes chart shapes from a closed list at capped confidence.
The product concept and the scoring model are my collaborator's. I built the platform around them - data layer, pipeline, database, access model, scheduled jobs, and the three-app architecture. Two people, pre-launch.
Why I Built It
Retail equity tools either publish a number with no derivation, or let a language model improvise a recommendation. Both are unfalsifiable; one of them sounds certain while being so.
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. Keeping it true across free data sources, a serverless 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 running product. It shipped as a single Next.js app - one deploy, no service boundary to maintain while the surface still moved. Once it stabilized, I reviewed it end to end, wrote up twelve ADRs, 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
Fitting a ~300-name universe scan into a serverless request budget.
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.
Degrading honestly when free sources fail without warning.
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 building a distributed monolith.
How it was solved
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