14 billion lab tests per year. The consumer experience ends at a confusing PDF. Otto reads your labs, tells you what matters, helps you buy what you need — and coaches you for free.
The lab testing industry is a $200B+ market with a 40-year-old user experience. Billions of reports per year, and the consumer journey ends at a confusing PDF. Companies like Function Health and Superpower are riding the wave of consumer-driven testing — but they sell the test and stop. Patients are left with more questions than answers, and most doctors don't have the time to walk through a 50-biomarker panel.
But the real business isn't in interpreting lab results. It's in what happens after. When a user learns their LDL is 165, they don't just want an explanation — they want to know what to take, whether it works, and where to get it. The gap between “you should consider a statin” and actually filling that prescription is weeks of friction, copays, and confusion. Otto eliminates that gap.
Otto is the new Walgreens. Instead of walking into a pharmacy with a prescription, you upload a lab report into a conversation. Otto reads it, explains what matters, educates you on the medications and supplements that could help, simulates how they'd shift your numbers — and lets you buy them without leaving the chat. The coaching is free. The revenue comes from pharma: supplement affiliate commerce at the point of deficiency discovery, contextual drug education worth $50+ per qualified lead, and eventually, telemedicine prescription fulfillment.
The moat compounds over time. Every user who stays 90 days generates paired intervention-outcome data — biomarker trajectories, protocols followed, daily adherence, and actual results. This is data that pharma companies would pay for, and that no one else has. At 10K users, Otto's recommendations are measurably better than guidelines alone. At 100K, it's the largest longitudinal biomarker-outcome dataset outside of clinical trials.
What follows is a detailed technical overview of how Otto works under the hood — the parsing pipeline, biological age models, recommendation engine, simulation, messaging agent, and infrastructure.
Update — March 2026: Everything described below has been built and deployed to production. This document now reflects what was actually shipped, not what was planned.
Core principle (preserved): Zero-friction entry, late-binding identity. PDF parsing and results live client-side until the user taps "Chat with Otto" — at which point a guest account is created and data is persisted. The Messenger agent delivers an interactive onboarding experience, then daily coaching. The iOS companion app syncs Apple Health data and provides a native dashboard.
The layered extraction pipeline was built exactly as planned, then expanded to support 70 biomarkers across international labs. Published as @ottolab/extraction on npm (MIT license).
BiomarkerSet with values, units, and flags.International/multilingual: The LLM handles Chinese, Japanese, Korean, Thai reports naturally. The SI fallback normalization layer converts international units automatically keyed by biomarker type.
Both planned models shipped and are published as @ottolab/bio-age on npm (MIT license, 100% test coverage). Ported to TypeScript from the ajsteele/bioage CC0 implementation and the BioAge R package.
| Model | Biomarkers | Status |
|---|---|---|
| Levine PhenoAge (2018) | 9 (CBC + CMP + hs-CRP) | Primary model. Shipped. 100% coverage. |
| KDM Biological Age | 10-12 (flexible selection) | Secondary model. Shipped. Handles missing biomarkers gracefully. |
Both models run on every lab report. The report card shows PhenoAge as the primary biological age, KDM as complementary, the user's chronological age (stored as birth year and computed dynamically), and the delta between biological and chronological age. Chronological age auto-updates as years pass — no stale data.
Built as a 748-line deterministic rules engine encoding AHA/ACC, ADA, and evidence-based clinical guidelines. The LLM synthesizes — it doesn't decide. Published as part of the API, generating 40+ protocols per report.
guideline-rules.ts)Deterministic protocol matching based on clinical practice guidelines: AHA/ACC statin logic with LDL thresholds, ADA Standards of Care with HbA1c tiers, CRP/vitamin D/homocysteine thresholds from meta-analyses. Each rule maps biomarker values to specific protocols with dosages and evidence citations. 100% test coverage.
llm-synthesizer.ts)Gemini 2.0 Flash personalizes the guideline-based recommendations into natural language, citing specific guidelines and PubMed references. The LLM explains why a protocol matters for this specific user. It does not decide the recommendation.
PubMed E-utilities (free, MeSH term search) pull supporting evidence for each recommendation. Systematic reviews and meta-analyses are surfaced as evidence citations alongside each protocol.
43-drug lookup table (drugs.ts) covering GLP-1 agonists, statins, metformin, rapamycin, thyroid, blood pressure, hormones, senolytics, NAD+ precursors, and more. Each entry includes brand names, mechanisms, affected biomarkers, prescription status, and direct purchase links (TrumpRx, Instalab). The Messenger agent auto-generates CTA buttons for supplement/medication purchases.
A proprietary simulation engine projects how protocols shift biomarkers over 365 days. This is Otto's core competitive advantage — kept private, not published.
| Module | Biomarkers |
|---|---|
| Lipid Panel | LDL, HDL, triglycerides |
| Metabolic Panel | HbA1c, fasting glucose, fasting insulin |
| Blood Pressure | Systolic, diastolic |
| Body Composition | Weight, BMI, body fat % |
| Uric Acid | Uric acid |
| Inflammatory Panel | hs-CRP, ESR |
Each module models drug effects (dose-response curves), lifestyle interventions (exercise, diet, weight loss), and natural physiological decay. The engine runs daily time steps with compounding effects. The web app renders the projection as an interactive Recharts chart; the Messenger agent sends a QuickChart.io image showing percentage improvement.
The identity model shipped with the planned multi-channel architecture, expanded to 22 tables.
| Table | Purpose |
|---|---|
users | Core identity — display_name, birth_year, timezone, status |
channel_identities | Multi-channel linking (messenger, whatsapp, app) with unique constraint |
lab_reports | Uploaded reports — biomarkers JSONB, bio_age_result, report_card |
recommendations | Protocol recommendations linked to lab reports |
conversations | Agent conversations — onboarding state machine, focus biomarker |
messages | Full chat history with skill_used tracking and metadata |
scheduled_tasks | BullMQ jobs — daily reminders, weekly summaries, follow-ups |
rate_limits | 30 msg/hr per channel user |
health_logs | Self-reported daily check-ins (sleep, movement, diet, stress) |
health_data | Apple HealthKit sync — steps, resting HR, sleep, body mass |
devices | Push notification tokens (APNs) |
app_link_tokens | Magic link auth tokens for mobile app |
waitlist | Email capture with unique constraint |
preview_tokens | Admin-generated beta access tokens |
audit_log | Every health data access logged — user, action, IP, timestamp |
All managed with Drizzle ORM (9 migrations applied). Row-level access control enforced — every query scoped by user_id from JWT. Full account deletion cascades all child tables.
Pivoted from WhatsApp to Messenger for faster go-to-market (no WhatsApp Business approval delays). Built with Meta Cloud API (Graph API v21.0) directly — no Twilio. The multi-channel adapter pattern was preserved: WhatsApp webhook is implemented and ready to activate.
When a user taps "Chat with Otto" from the web app, they land in Messenger via an m.me referral link. Otto delivers a 5-phase interactive onboarding:
State tracked via conversations.onboarding_phase column. 90-second nudge timeout (BullMQ delayed job) re-prompts if user doesn't tap. Free-text during onboarding routes to the agent normally, then auto-advances the phase.
| Skill | Trigger |
|---|---|
| Lab Interpreter | New report uploaded |
| Health Q&A | Default — any health question |
| Drug Education | 43 drug/medication keyword patterns |
| Simulate | "simulate" / "what if" keywords |
| Product Recommender | "where to buy" / product queries |
| Protocol Reminder | Scheduled daily (personalized, humanized) |
| Weekly Summary | Scheduled Monday (180-day simulation + trend charts) |
| Escalation | Emergency keywords → "consult your doctor" |
Routing is keyword-based regex (no LLM overhead). Messages auto-split at 2,000 characters at paragraph/sentence boundaries. Product CTA buttons auto-generated from [PRODUCT:name] tags in LLM responses — builds TrumpRx/Instalab purchase link + "Ask My Doctor" postback. Buttons sent as a separate message to respect Messenger's 640-character template limit.
Beyond reactive Q&A, Otto proactively coaches users daily. A BullMQ scheduler fires timezone-aware reminders each morning, cycling through health topics on a 4-day rotation.
Originally planned as V2 (post-messaging validation). Shipped alongside the Messenger agent as React Native (Expo SDK 52+) instead of SwiftUI — faster iteration, shared TypeScript codebase with the API. Same backend API, no duplicate business logic.
Magic link flow (Messenger → ottolab://link/:token deep link → JWT stored in SecureStore) + phone OTP fallback. Universal links (otto.kevinho.com/app/link/:token) supported via apple-app-site-association.
Home screen with animated glass-morphism folder cards ("Lab Reports" with count, "Connections" with Apple Health status). Pull-to-refresh, notification bell. Settings with editable name and calendar age (stored as birth year, auto-updates yearly).
Thumbnail grid of all uploaded reports with stagger animation. Detail view shows PhenoAge/KDM bio age, simulation improvement chart, all 70 parsed biomarkers grouped into 10 categories (Lipids, Metabolic, Kidney, Liver, Inflammatory, Thyroid, Hormones, Hematology, Vitals, Vitamins) with color-coded status dots.
EAS Build with dev/preview/production profiles. APNs push notifications for silent sync triggers. 130+ test cases in TEST_PLAN.md covering 11 sections.
Every security requirement from the original strategy was implemented:
user_id from JWTaudit_log table (user, action, resource, IP)DELETE /api/me cascades all 22 tables, clears auth tokens
Legal pages (privacy policy, terms of service) served at otto.kevinho.com/privacy and otto.kevinho.com/terms.
The production stack diverged from the original plan in two ways: Railway replaced Cloud Run (simpler deployment, integrated Postgres/Redis), and Messenger shipped before WhatsApp.
| Layer | Planned | Shipped |
|---|---|---|
| Frontend | Next.js | Next.js 15 (App Router, TypeScript, Tailwind v4, Zustand) |
| Mobile | SwiftUI (V2) | React Native (Expo SDK 52+, TypeScript) |
| API | Node.js + Fastify | Fastify (TypeScript strict, Zod validation) |
| ORM | — | Drizzle ORM (22 tables, 9 migrations) |
| Database | PostgreSQL | PostgreSQL 16 (Railway) |
| Cache/Queue | Redis + BullMQ | Redis 7 + BullMQ (Railway) |
| AI/LLM | Multi-provider | Google Gemini (2.5 Flash Lite + 2.0 Flash) via provider abstraction. Anthropic Claude swap-ready. |
| Messaging | WhatsApp (Meta Cloud API) | Messenger (Meta Graph API v21.0). WhatsApp webhook ready. |
| Auth | Phone OTP | Phone OTP + magic link (JWT HS256, SecureStore on mobile) |
| Hosting | Vercel + Cloud Run | Vercel (web) + Railway (API + worker + Postgres + Redis) |
| CI/CD | GitHub Actions | GitHub Actions (lint + test on push/PR, Postgres + Redis services) |
| — | Resend (waitlist welcome emails) |
Three public npm packages provide the foundational health logic. Two marketplace extensions distribute Otto as an AI skill for existing platforms.
| Package | Purpose | Coverage |
|---|---|---|
@ottolab/bio-age |
PhenoAge + KDM biological age calculation | 100% |
@ottolab/extraction |
Lab PDF/CSV parsing, validation, 70 biomarkers | 100% |
@ottolab/shared |
Shared types + constants (workspace package) | — |
Extensions import shared logic via public npm packages. Private components (simulation engine, agent, messaging) stay in the monorepo.
16 route handlers registered, serving 25+ endpoints across public, admin, auth, and authenticated scopes.
| Scope | Endpoints |
|---|---|
| Public (no auth) | GET /health, POST /api/parse, POST /api/report, POST /api/recommend, POST /api/simulate, POST /api/waitlist |
| Admin | POST /api/admin/preview-tokens, GET /api/preview-tokens/validate |
| Auth | POST /api/auth/otp, POST /api/auth/verify, POST /api/app-link/:token |
| Authenticated | GET/PATCH/DELETE /api/me, GET /api/me/reports, GET /api/me/reports/:id, POST /api/me/health-data, GET /api/me/health-data, POST /api/me/devices, GET /api/me/export, POST /api/me/notify-connection, POST /api/sessions |
| Webhooks | GET/POST /api/webhook/messenger, GET/POST /api/webhook/whatsapp |
Response format: { data: T } on success, { error: { code, message } } on error. All responses include X-Request-Id header. 80%+ route test coverage.
Business
Technical