# nLight.fit — complete documentation Every documentation page concatenated in reading order. Canonical HTML lives at https://nlight.fit/docs. --- # Overview nLight.fit is a personal fitness and life tracking platform with an AI coach attached to it. The tracking half is deliberately plain: a spreadsheet-style ledger where you confirm your day. Did you wake at seven, take your vitamins, read, train, eat well, weigh in. The coaching half is not plain at all — it reads that ledger in full, alongside your WHOOP data, your bloodwork, your genetics and everything you have told it in past conversations, and answers questions grounded in what actually happened rather than in general advice. The central idea is that a coach is only as good as its memory. Most AI fitness tools summarise your history into a paragraph and reason from that paragraph. This one keeps the record at full resolution and retrieves against it. ## What it does **A commitment ledger.** One row per day, one column per thing you track. Standard columns for habits, fitness, and nutrition; custom columns for whatever else matters to you. Daily, weekly or monthly check-in rhythms, depending on how you want to work. **An AI coach.** Conversational, streaming, and grounded in your data. It classifies what you are asking, decides how much history and how much reasoning the question warrants, retrieves the relevant days and summaries, and calls tools when it needs a precise number rather than a recollection. **A voice agent.** The same coach over a real-time voice connection, with modes for open conversation, hands-free data entry, and guided onboarding. **Health integrations.** WHOOP recovery, sleep and strain synced automatically. Bloodwork panels extracted from PDF lab reports. Methylation genetics as a one-time profile that explains mechanisms rather than tracking a trend. **A correlation engine.** Runs nightly across every module — habits, nutrition, wearables, labs, genetics — with partial-correlation controls and multiple-testing correction, so what surfaces as a pattern has survived some scrutiny. **An MCP server.** Read-only access to all of it from Claude, Cursor, or any MCP client, using the same tool handlers the in-app coach uses. ## Who it is for Someone who already tracks, or wants to, and is frustrated that the data never turns into an answer. The platform assumes you will log honestly and consistently; almost everything interesting it does depends on having a real history to reason over. ## Availability **Early access, approval-only, free.** nLight.fit is pre-GA. Accounts are granted individually rather than by open signup, and there is no paid tier — quality is not something the product rations. Request access from [the landing page](/#early-access). ## How it is built A short version, because it explains some of the behaviour you will encounter: - **Vercel serverless functions** for the backend, **MongoDB Atlas** as the single source of truth. There is no client-side cache of your data; every change is written immediately and every read comes from the database, so two devices never disagree. - **xAI Grok** for coaching and extraction, **xAI Realtime** for voice, **VoyageAI** for embeddings and reranking. - **Hybrid retrieval** — vector search and full-text search fused, then reranked — so a question about something you wrote eight months ago can actually find it. - **Nightly and intraday cron workers** that compute summaries, personal records, correlations and insights ahead of time, so the coach reads a precomputed answer instead of deriving one mid-conversation. The [architecture](/docs/data-model) and [security](/docs/security) pages go further. ## Where to go next - [Quickstart](/docs/quickstart) — from account to first coached conversation - [Concepts](/docs/concepts) — the vocabulary the rest of the docs assumes - [The ledger](/docs/ledger) — how daily tracking actually works - [MCP server](/docs/mcp) — connect Claude or Cursor to your own data --- # Quickstart About ten minutes of setup, then the useful part starts accumulating on its own. ## 1. Get access nLight.fit is approval-only during early access. Request an account from [the landing page](/#early-access). Once approved, sign in at [nlight.fit/app](/app). ## 2. Tell the coach who you are Open settings and fill in your profile. This is the single highest-leverage ten minutes you will spend in the product, because almost every coaching behaviour keys off it: your goal and target, your training constraints and equipment, injuries, diet approach and allergies, the coaching style you respond to, what has worked for you before and what has failed. You can also do this by voice. The onboarding voice mode walks through the profile conversationally, which is considerably faster than filling in a long form. Nothing here is required to start tracking. But a coach that does not know your goal can only give you general advice, which is the thing this product exists not to do. ## 3. Write your Mission Statement A goal in one sentence, plus three commitments that would actually get you there. This is separate from the profile on purpose. The profile describes your situation; the Mission Statement declares your intent, and the platform measures against it — tracking progress, and testing whether your three commitments are in fact the behaviours moving the outcome. ## 4. Confirm your first day Open the ledger and fill in today's row. Habits are checkboxes, numbers are numbers, meals are free text. Two things to know: - **Every edit saves immediately.** There is no save button, and no draft state to lose. - **Custom columns.** Add anything the standard set does not cover — "No Alcohol", "Sauna", "Cold Plunge", whatever you actually care about. Give it a description; the coach reads descriptions, and a column called `GMAD` means nothing without one. ## 5. Connect what can fill itself in **WHOOP**, if you wear one. Authorize once in settings and recovery, HRV, sleep stages, strain and workouts sync on a schedule and appear as columns alongside what you type. See [WHOOP](/docs/whoop). **Bloodwork**, if you have a recent panel. Upload the PDF; the markers are extracted into a structured record with trends. See [labs](/docs/labs). **Methylation genetics**, if you have had the test. One upload, once ever. See [methylation](/docs/methylation). ## 6. Talk to the coach Open the AI Coach and ask something real. Good first questions: - "What should I focus on this week?" - "Am I actually making progress toward my goal, or does it just feel that way?" - "What's getting in the way of my sleep?" The coach knows what you have logged, what you said in settings, and what your Mission Statement commits you to. Early on it will have little history to reason over and will say so; that is the honest answer, not a limitation being hidden. ## 7. Give it a week The parts of the platform that are genuinely worth having — correlations, personal records, momentum, changepoint detection, goal ETA — need data before they mean anything. A correlation computed on four days is noise, and the engine will decline to report it rather than dress it up. A week gets you basic patterns. A month gets you correlations with real confidence. Bloodwork becomes interesting on the second panel, when there is a trend rather than a snapshot. ## 8. Optional: connect an AI client If you use Claude Code, Claude Desktop or Cursor, you can query your own record from there over the [MCP server](/docs/mcp). Mint a token with the [management scripts](/docs/token-scripts), add the server following [client setup](/docs/clients), and your coding agent can pull streaks, correlations and the raw feature matrix directly. ## Related - [Concepts](/docs/concepts) — the vocabulary used throughout these docs - [The ledger](/docs/ledger) — columns, frequencies, CSV import - [AI coach](/docs/ai-coach) — how it decides what to read and how hard to think --- # Concepts The terms below appear throughout the documentation, the product and the tool descriptions. Several of them mean something specific here that they do not mean generally. ## Ledger The daily tracking table. One row per date, one column per tracked thing. Called a ledger rather than a log because the framing is confirmation — you are affirming what you did against what you committed to, not journaling. ## Metric A single tracked quantity, identified by a canonical id such as `whoopHrv` or `pushUps`. Every metric is declared once in the **metric registry**, which is the shared source of truth for the tracking table, the API, the correlation engine and the AI tools. The [metric reference](/docs/metrics) is generated directly from it. A metric has a value type (binary, number, select, time, text), an optional unit, a set of aliases so `hrv` and `push-ups` resolve correctly, and — if it participates in analysis — a domain and a role. ## Derived metric A metric computed from other fields rather than stored. `_exercise` is true when a real workout is logged or any reps were done. `_sleepHours` is the sum of the WHOOP sleep stages. They have no column in the table but are first-class for analysis. Derived ids are prefixed with an underscore. ## Behavior and outcome Every metric in the analytical catalog is tagged as one or the other, or both. A **behavior** is something you do — vitamins, reading, training time, sleep consistency. An **outcome** is something that happens to you — recovery, HRV, weight, resting heart rate. The distinction drives correlation direction. "Does X affect Y" only makes sense when X is a behavior, and the engine will not present an outcome-to-outcome association as though one caused the other. ## Custom column A metric you define yourself. It gets an id, a label and a description. The description matters more than people expect: it is what the coach reads to understand what the column means, and it is what fuzzy matching searches when you ask about "nicotine" and the column is called "No Zyn". ## Check-in frequency How often you sit down and confirm. Set it to **daily** and each row is a day, navigated month by month. Set it to **weekly** and each row is a week, navigated by quarter. Set it to **monthly** and each row is a month, navigated by year. This changes the row granularity of the whole ledger, not the display density of one column, and it also sets the period the AI summarizes over. ## Data-quality gating Some values arrive with caveats attached, and the platform reads the caveats rather than only the numbers. A WHOOP recovery score produced during the calibration period is not a measurement, and sleep stages from a night the strap spent an hour off your wrist are a partial record, not a short night. Both are suppressed for analysis while remaining visible in the table exactly as WHOOP reported them. Correlating either as if it were clean data manufactures relationships that are not there. ## Feature store A precomputed daily matrix: every metric normalized to a number, plus its z-score against a trailing 30-day baseline. It is what the correlation engine reads and what `get_daily_features` returns. Keeping it separate from the raw ledger means analysis never has to re-derive the same normalization on every query. ## Correlation graph Relationships between metrics stored as edges rather than prose. Each edge carries the correlation coefficient, the partial correlation after controlling for confounders, the lag, the p-value, the sample size, a confidence tier and its provenance. Because it is a graph, it can be traversed: depth 1 gives the edges touching a metric, depth 2 gives one-hop chains through an intermediate — sleep to recovery to next-day strain capacity — which is how a mechanistic story gets assembled instead of asserted. ## Confidence tier How much weight an edge deserves. **Confirmed** relationships are known physiology that held up in your data. **Contradicted** ones are known physiology that did not, which is often the more interesting result. **Robust** is reserved for edges that survived both confounder controls and multiple-testing correction. A relationship computed on demand for a pair nobody precomputed is a **hypothesis**, and is never promoted above that no matter how strong the coefficient looks. ## Provenance envelope A `{ asOf, n, confidence, tier, source }` block attached to quantitative tool results. It exists so a model weighing two numbers can tell that one rests on 90 days and the other on six, rather than treating every figure as equally solid. ## Learnings Insights the coach extracts from conversation and keeps. "Travels for work most of March." "Responds badly to being told to push harder." "Shoulder flares up on overhead press." Learnings are categorized, scored for confidence, embedded for semantic retrieval, and reinforced when you mention them again. They also **decay** — an insight that stops being mentioned loses weight and is eventually pruned — and they **consolidate**, so five near-identical observations merge into one rather than crowding out everything else. There is a hard cap per user, which is what keeps memory from becoming noise. ## Mission Statement A structured declaration of intent: one goal, three commitments. Distinct from the profile, which describes your situation. The platform tracks progress against the goal and analyzes whether the three commitments are actually the behaviours moving it — which is a question it can answer, because it has the correlation graph. ## Context Engine The layer that assembles what the coach knows before it answers. Temporal awareness (what day and time it is for you), momentum (whether you are building or slipping), lifecycle stage (whether you are new enough that the honest answer is "not enough data yet"), and which parts of your profile are still blank. ## Smart payload The mechanism that decides how much to spend on a question. It classifies the query, then picks the model, the reasoning effort and the amount of history to load. "What did I weigh Tuesday" and "why has my recovery been declining since April" should not cost the same or read the same amount of your record. ## Summaries Narrative rollups written ahead of time by the summarization pipeline: weekly, monthly, quarterly and yearly reviews, lab interpretations, mission analysis, personal records, and the cross-module correlation narrative. Retrieval searches these alongside individual days, so a question about a period finds the period rather than reassembling it from ninety rows. ## Embedding and hybrid retrieval Every day and every summary is embedded as a vector. A question runs both vector search and full-text search, the two result sets are fused, and the survivors are reranked before anything reaches the model. Vector search alone misses exact terms; text search alone misses paraphrase. Running both and fusing them is what makes "that week I was sick in February" findable. ## Related - [Metric reference](/docs/metrics) — every metric, generated from the registry - [The ledger](/docs/ledger) — how tracking works day to day - [Analytics](/docs/analytics) — what the engines compute and when --- # The ledger The ledger is the tracking table, and it is the foundation everything else reads from. One row per period, one column per tracked thing. It is intentionally spreadsheet-shaped. A grid you can scan gives you something a stack of cards does not: you can see a gap, a streak or a slide with your eyes, before any analysis is involved. ## Columns The standard set covers habits (wake by 7am, reading, vitamins), fitness (weight, push-ups, sit-ups, mile time, stairs, workout session) and nutrition (breakfast, lunch, dinner, snacks, an overall eating-quality rating). Every one of them, with types and units, is listed in the [metric reference](/docs/metrics). Columns can be hidden, reordered, and resized. All of that is stored server-side against your account, so the layout you built on your laptop is the layout you get on your phone. ### Custom columns Add columns for whatever the standard set does not cover. Each one gets a label and a description. **Write the description properly.** It is not decoration. The coach reads it to understand what the column means, and fuzzy matching searches it — which is why asking about "nicotine" finds a column named "No Zyn", and why a column called "GMAD" with no description is opaque to everything downstream. ## Check-in frequency Three rhythms, and the choice changes the shape of the table rather than just its density: | Frequency | Each row is | You navigate by | Default window | | --- | --- | --- | --- | | Daily | A day | Month | 2 weeks | | Weekly | A week | Quarter | A quarter | | Monthly | A month | Year | A year | It also sets the period the AI summarizes over, so a weekly tracker gets weekly narratives rather than daily ones. ## Saving There is no save button. Every edit writes immediately to the database, field by field, with a brief indicator confirming it landed. This follows from a deliberate architectural choice: MongoDB is the single source of truth and the browser keeps essentially no cache of your data. The cost is that the ledger needs a connection. The benefit is that two devices can never disagree, a refresh can never lose work, and there is no sync conflict to resolve because there is nothing to sync. Saving a field also triggers two background actions: the day is re-embedded so semantic search stays current, and cached computations for your account are invalidated so the coach does not answer from a stale figure. ## WHOOP columns If WHOOP is connected, its metrics can be shown as columns in the same table, merged into the same rows. You choose which ones — the full set is large, and most people want recovery, HRV, sleep and strain rather than all forty. See [WHOOP](/docs/whoop). ## Import and export **CSV export** produces the raw ledger over any date range, headers matching the canonical field names. **CSV import** maps incoming headers onto known fields and aliases, so an export from a previous tool usually lands without hand-editing. Imports go through a bulk upsert path, which is rate limited more tightly than normal edits because a single call can write hundreds of days. Importing history is worth doing if you have it. The correlation engine, personal records and goal projections all improve with length, and backfilled days are treated identically to days entered live. ## Progress pictures Photos attach to the record alongside the numbers, stored per user and retrievable as a series. Weight is a lagging and noisy signal; a monthly photo is often the clearer one. ## Related - [Metric reference](/docs/metrics) — every field, type and unit - [Concepts](/docs/concepts) — derived metrics, quality gating, the feature store - [AI coach](/docs/ai-coach) — what the coach does with all of this --- # Metric reference Generated from the metric registry, which is the single source of truth shared by the tracking table, the API, the correlation engine and the AI tools. Registry version **2**, **59 metrics**. `id` is the canonical field name. It is what you pass to any tool that takes a metric, and what appears as a key in stored day data. ## Core Fields you fill in yourself, in the tracking table or by voice. | id | Type | Unit | Analytical role | Description | | --- | --- | --- | --- | --- | | `date` | date | — | — | Calendar date of the entry | | `wakeAt7am` | binary | — | behavior (habits) | Woke up by 7am (yes/no) | | `reading30min` | binary | — | behavior (habits) | Read for at least 30 minutes (yes/no) | | `takeVitamins` | binary | — | behavior (habits) | Took daily vitamins/supplements (yes/no) | | `weight` | number | lbs | outcome (fitness) | Body weight in pounds (lbs) | | `pushUps` | number | reps | outcome (fitness) | Number of push-ups completed | | `sitUps` | number | reps | outcome (fitness) | Number of sit-ups completed | | `oneMileRun` | time | MM:SS | outcome (fitness) | Time to run one mile (MM:SS format) | | `stairsClimbed` | number | flights | outcome (fitness) | Number of floors/flights of stairs climbed | | `workoutSession` | select | — | — | Type of workout or activity status for the day | | `breakfast` | text | — | — | What was eaten for breakfast | | `lunch` | text | — | — | What was eaten for lunch | | `dinner` | text | — | — | What was eaten for dinner | | `snacks` | text | — | — | Snacks eaten throughout the day | | `healthyEating` | select | — | — | Self-assessment of overall eating quality for the day | ## WHOOP Pulled from WHOOP on a schedule. Present only when the integration is connected. | id | Type | Unit | Analytical role | Description | | --- | --- | --- | --- | --- | | `whoopRecovery` | number | % | outcome (whoop) | WHOOP Recovery score (0-100%). Green 67+, Yellow 34-66, Red <34. Indicates body readiness to perform. | | `whoopHrv` | number | ms | outcome (whoop) | Heart rate variability in milliseconds (rmssd). Higher = better recovery and lower stress. | | `whoopSleep` | number | % | outcome (whoop) | Sleep performance percentage. 100% = full sleep need met. | | `whoopStrain` | number | — | both (whoop) | Daily strain score (0-21). Light 0-9, Moderate 10-13, Hard 14-17, All-out 18+. | | `whoopRhr` | number | bpm | outcome (whoop) | Resting heart rate in bpm measured during sleep. Lower trending = improving cardiovascular fitness. | | `whoopDeepSleep` | number | hours | outcome (whoop) | Hours of slow-wave (deep) sleep. Critical for physical recovery and muscle repair. | | `whoopCalories` | number | kcal | outcome (whoop) | Total calories burned for the day including BMR and activity. | | `whoopSleepEfficiency` | number | % | outcome (whoop) | Sleep efficiency - percentage of time in bed actually spent sleeping. >85% is good. | | `whoopDisturbances` | number | — | outcome (whoop) | Number of sleep disturbances detected. Lower = better sleep quality. | | `whoopSpo2` | number | % | outcome (whoop) | Blood oxygen saturation percentage. Normal is 95-100%. Drops can indicate illness. | | `whoopRespRate` | number | breaths/min | outcome (whoop) | Respiratory rate during sleep (breaths/min). Elevated can indicate illness or stress. | | `whoopRemSleep` | number | hours | outcome (whoop) | Hours of REM sleep. Critical for cognitive recovery, memory consolidation, and emotional processing. | | `whoopLightSleep` | number | hours | outcome (whoop) | Hours of light sleep (stages 1-2). Transition sleep important for overall rest architecture. | | `whoopSleepCycles` | number | — | outcome (whoop) | Number of complete sleep cycles (each ~90 min). 4-6 cycles is optimal for full restoration. | | `whoopSkinTemp` | number | °C | outcome (whoop) | Skin temperature during sleep. Deviation above personal baseline often precedes illness by a day or two. | | `whoopAvgHr` | number | bpm | outcome (whoop) | Average heart rate across the whole day, workouts included. Rising at constant strain suggests overreaching or illness. | | `whoopMaxHr` | number | bpm | outcome (whoop) | Highest heart rate reached during the day, including efforts never logged as a workout. | | `whoopTimeInBed` | number | hours | both (whoop) | Total hours spent in bed, asleep or not. Compare against sleep hours to see time lost to restlessness. | | `whoopAwakeTime` | number | min | outcome (whoop) | Minutes spent awake between falling asleep and getting up. Under 20 is settled; over 40 suggests a disrupted night. | | `whoopSleepConsistency` | number | % | behavior (whoop) | How closely bed and wake times matched the recent pattern. Circadian regularity predicts next-day recovery about as strongly as duration does. | | `whoopSleepNeed` | number | hours | outcome (whoop) | WHOOP's calculated sleep requirement for the night: personal baseline plus what recent debt, strain, and naps add to it. | | `whoopSleepDebt` | number | min | both (whoop) | Accumulated sleep shortfall carried into the night, in minutes of extra sleep owed. | | `whoopBedtime` | time | — | — | Local clock time the wearer fell asleep (HH:MM, 24-hour). | | `whoopWakeTime` | time | — | — | Local clock time the wearer woke (HH:MM, 24-hour). | | `whoopNapTime` | number | min | behavior (whoop) | Total minutes napped during the day, excluded from night-sleep stage totals. | | `whoopNapCount` | number | — | behavior (whoop) | Number of naps recorded during the day. | | `whoopWeight` | number | lbs | outcome (whoop) | Body weight as recorded on the WHOOP profile, in pounds. Independent of the manually tracked weight column. | | `whoopWorkouts` | list | — | — | Per-session workout detail: sport, start time, duration, strain, heart rate, calories, distance, and HR zone split. | | `whoopWorkoutCount` | number | — | behavior (fitness) | Number of workouts recorded by WHOOP during the day. | | `whoopWorkoutTime` | number | min | behavior (fitness) | Total minutes of recorded workout time across all sessions. | | `whoopWorkoutDistance` | number | mi | behavior (fitness) | Total distance covered across recorded workouts, in miles. | | `whoopCalibrating` | binary | — | — | Set while WHOOP is still establishing the wearer's baseline, which makes that day's recovery score provisional rather than measured. | | `whoopSleepNoData` | number | min | — | Minutes of the night the strap recorded nothing (taken off, or a sensor dropout). Every sleep-stage total is understated by this much. | | `whoopZone1` | number | min | behavior (fitness) | Minutes in HR Zone 1 (50-60% max HR). Recovery/warm-up zone. | | `whoopZone2` | number | min | behavior (fitness) | Minutes in HR Zone 2 (60-70% max HR). Fat-burning/base aerobic zone. | | `whoopZone3` | number | min | behavior (fitness) | Minutes in HR Zone 3 (70-80% max HR). Aerobic/tempo zone. | | `whoopZone4` | number | min | behavior (fitness) | Minutes in HR Zone 4 (80-90% max HR). Threshold/anaerobic zone. | | `whoopZone5` | number | min | behavior (fitness) | Minutes in HR Zone 5 (90-100% max HR). Max effort/VO2max zone. | ## AI-enriched nutrition Derived from your logged meals by the nightly enrichment pass. | id | Type | Unit | Analytical role | Description | | --- | --- | --- | --- | --- | | `mealQualityScore` | number | — | behavior (nutrition) | AI-estimated meal quality score for the day. | | `mealProteinEst` | number | g | behavior (nutrition) | AI-estimated protein intake from logged meals. | | `mealUltraProcessed` | binary | — | behavior (nutrition) | AI-detected ultra-processed eating day (yes/no). | ## Derived Computed from other fields. These have no column in the table, but they are first-class metrics for analysis. | id | Type | Unit | Analytical role | Description | | --- | --- | --- | --- | --- | | `_exercise` | binary | — | behavior (fitness) | Derived: any exercise happened (workout logged or reps done). | | `_healthyEating` | number | — | behavior (nutrition) | Derived: manual eating-quality rating as ordinal 0/1/2. | | `_sleepHours` | number | hours | behavior (whoop) | Derived: total sleep hours from WHOOP sleep stages. | ## Retrieval catalog **47** metrics are analysable, meaning `get_daily_features` can return them as a numeric series with z-scores. This is the set it returns when you do not name specific metrics. Your own custom columns are analysable too and are added to this set per account, so they cannot be listed here. Call `list_metrics` for the catalog as it applies to you. | id | Label | Domain | Role | In discovery scan | | --- | --- | --- | --- | --- | | `wakeAt7am` | 7am wake | habits | behavior, binary | yes | | `reading30min` | Reading | habits | behavior, binary | yes | | `takeVitamins` | Vitamins | habits | behavior, binary | yes | | `weight` | Weight | fitness | outcome | yes | | `pushUps` | Push-ups | fitness | outcome | yes | | `sitUps` | Sit-ups | fitness | outcome | no — logged too rarely for a daily correlation to mean anything | | `oneMileRun` | Mile time | fitness | outcome | no — logged too rarely for a daily correlation to mean anything | | `stairsClimbed` | Stairs climbed | fitness | outcome | no — logged too rarely for a daily correlation to mean anything | | `whoopRecovery` | Recovery | whoop | outcome | yes | | `whoopHrv` | HRV | whoop | outcome | yes | | `whoopSleep` | Sleep performance | whoop | outcome | yes | | `whoopStrain` | Strain | whoop | both | yes | | `whoopRhr` | Resting HR | whoop | outcome | yes | | `whoopDeepSleep` | Deep sleep | whoop | outcome | yes | | `whoopCalories` | Calories burned | whoop | outcome | no — derived from heart rate, so it restates strain | | `whoopSleepEfficiency` | Sleep efficiency | whoop | outcome | yes | | `whoopDisturbances` | Sleep disturbances | whoop | outcome | yes | | `whoopSpo2` | SpO2 | whoop | outcome | yes | | `whoopRespRate` | Respiratory rate | whoop | outcome | yes | | `whoopRemSleep` | REM sleep | whoop | outcome | yes | | `whoopLightSleep` | Light sleep | whoop | outcome | no — largely total sleep minus deep and REM, which are both present | | `whoopSleepCycles` | Sleep cycles | whoop | outcome | no — tracks total sleep duration almost exactly | | `whoopSkinTemp` | Skin temperature | whoop | outcome | yes | | `whoopAvgHr` | Average HR | whoop | outcome | no — moves with strain, which is present and better understood | | `whoopMaxHr` | Peak HR | whoop | outcome | no — a workout artifact rather than a daily signal | | `whoopTimeInBed` | Time in bed | whoop | both | no — sleep hours plus awake time, both present | | `whoopAwakeTime` | Awake time | whoop | outcome | no — the inverse of sleep efficiency, which is present | | `whoopSleepConsistency` | Sleep consistency | whoop | behavior | yes | | `whoopSleepNeed` | Sleep need | whoop | outcome | no — computed by WHOOP from recent debt, strain and naps | | `whoopSleepDebt` | Sleep debt | whoop | both | no — computed by WHOOP from the sleep history it would be correlated against | | `whoopNapTime` | Nap time | whoop | behavior | yes | | `whoopNapCount` | Nap count | whoop | behavior | yes | | `whoopWeight` | Weight (WHOOP) | whoop | outcome | no — duplicates the manually tracked weight column | | `whoopWorkoutCount` | Workout count | fitness | behavior | no — training load is represented by strain and training time | | `whoopWorkoutTime` | Training time | fitness | behavior | yes | | `whoopWorkoutDistance` | Workout distance | fitness | behavior | no — sport-dependent, so it is not comparable across days | | `whoopZone1` | HR zone 1 minutes | fitness | behavior | no — strain and calories are computed from HR-zone minutes | | `whoopZone2` | HR zone 2 minutes | fitness | behavior | no — strain and calories are computed from HR-zone minutes | | `whoopZone3` | HR zone 3 minutes | fitness | behavior | no — strain and calories are computed from HR-zone minutes | | `whoopZone4` | HR zone 4 minutes | fitness | behavior | no — strain and calories are computed from HR-zone minutes | | `whoopZone5` | HR zone 5 minutes | fitness | behavior | no — strain and calories are computed from HR-zone minutes | | `mealQualityScore` | Meal quality (AI) | nutrition | behavior | yes | | `mealProteinEst` | Protein intake | nutrition | behavior | yes | | `mealUltraProcessed` | Ultra-processed eating | nutrition | behavior, binary | yes | | `_exercise` | Exercise | fitness | behavior, binary | yes | | `_healthyEating` | Eating quality (manual) | nutrition | behavior | yes | | `_sleepHours` | Sleep hours | whoop | behavior | yes | ## Discovery catalog Of those, **27** form the discovery catalog — the pre-registered set the nightly correlation engine tests against each other. It is narrower than the retrieval catalog on purpose. The engine controls its false discovery rate across the whole batch of candidate pairs, so every metric added to the scan makes the evidence bar higher for every other metric in it. Definitional relationships (calories burned against strain), near-duplicates, and metrics too sparse to estimate are excluded so the budget goes to hypotheses that could actually be wrong. A metric outside this set is still fully retrievable — you can test it yourself with `get_daily_features`. It simply has no precomputed verdict in `get_correlations`. ## Aliases Tools that take a metric name accept aliases as well as canonical ids, so `hrv` resolves to `whoopHrv` and `push-ups` to `pushUps`. Custom columns you create are matched fuzzily by label and description. | Canonical id | Accepted aliases | | --- | --- | | `wakeAt7am` | `7am`, `wake`, `wake7am`, `wakeat7am` | | `reading30min` | `read`, `reading`, `reading30min` | | `takeVitamins` | `takevitamins`, `vitamin`, `vitamins` | | `weight` | `weight` | | `pushUps` | `push ups`, `push-ups`, `pushups` | | `sitUps` | `sit ups`, `sit-ups`, `situps` | | `oneMileRun` | `mile`, `miletime`, `onemilerun` | | `stairsClimbed` | `stairs`, `stairsclimbed` | | `workoutSession` | `exercise`, `workout`, `workoutsession` | | `healthyEating` | `eating`, `healthy eating`, `healthyeating` | | `whoopRecovery` | `recovery` | | `whoopHrv` | `hrv` | | `whoopSleep` | `sleepperf`, `sleepperformance` | | `whoopStrain` | `strain` | | `whoopRhr` | `restingheartrate`, `rhr` | | `whoopDeepSleep` | `deepsleep` | | `whoopCalories` | `calories` | | `whoopSleepEfficiency` | `sleepefficiency` | | `whoopDisturbances` | `disturbances` | | `whoopSpo2` | `bloodoxygen`, `spo2` | | `whoopRespRate` | `respiratoryrate`, `resprate` | | `whoopRemSleep` | `rem`, `remsleep` | | `whoopLightSleep` | `lightsleep` | | `whoopSleepCycles` | `sleepcycles` | | `whoopSkinTemp` | `skintemp`, `skintemperature`, `temperature` | | `whoopAvgHr` | `averageheartrate`, `avghr` | | `whoopMaxHr` | `maxheartrate`, `maxhr`, `peakhr` | | `whoopTimeInBed` | `inbed`, `timeinbed` | | `whoopAwakeTime` | `awaketime`, `timeawake`, `wakeaftersleeponset` | | `whoopSleepConsistency` | `consistency`, `sleepconsistency` | | `whoopSleepNeed` | `neededsleep`, `sleepneed` | | `whoopSleepDebt` | `debt`, `sleepdebt` | | `whoopBedtime` | `bedtime`, `sleepstart`, `wenttobed` | | `whoopWakeTime` | `sleepend`, `waketime`, `wokeup` | | `whoopNapTime` | `napminutes`, `naptime` | | `whoopNapCount` | `napcount`, `naps` | | `whoopWeight` | `strapweight`, `whoopweight` | | `whoopWorkouts` | `whoopworkouts`, `workouts` | | `whoopWorkoutCount` | `sessions`, `workoutcount` | | `whoopWorkoutTime` | `trainingtime`, `workouttime` | | `whoopWorkoutDistance` | `distance`, `workoutdistance` | | `whoopCalibrating` | `calibrating`, `usercalibrating` | | `whoopSleepNoData` | `nodata`, `sleepgap`, `sleepnodata` | | `whoopZone1` | `zone1` | | `whoopZone2` | `zone2` | | `whoopZone3` | `zone3` | | `whoopZone4` | `zone4` | | `whoopZone5` | `zone5` | | `mealQualityScore` | `mealquality` | | `mealProteinEst` | `protein`, `proteinintake` | | `mealUltraProcessed` | `ultraprocessed` | --- # AI coach The coach is a conversation with something that has read your entire record. Not a summary of it — the record. ## What happens when you ask something Roughly, in order: **The input is screened.** A prompt guard checks for injection attempts before anything reaches the model. This matters more than usual here, because the coach reads text you did not necessarily write — meal notes, imported data, extracted PDF content. **The question is classified.** Complexity, time scope, and what kind of answer it wants. "What did I weigh Tuesday" and "why has my recovery been sliding since April" are different problems and should not be solved the same way. **Model and effort are selected.** The classification picks the reasoning depth and how much of your history to load. This is the *smart payload* layer, and it is the reason a simple lookup returns in a couple of seconds while a genuine analysis takes longer and reads considerably more. **Context is assembled in parallel.** Recent ledger data, relevant summaries, goal progress, semantically matched learnings, streaks, personal records, correlations, trigger insights. **The Context Engine enriches it.** Temporal awareness — what day and time it actually is for you. Momentum — whether you are building or slipping. Lifecycle stage — whether you are new enough that "not enough data yet" is the honest answer. Profile gaps, so it can tell when it is reasoning without something it needs. **Retrieval runs, if the question is historical.** Vector search and full-text search over your days and summaries, fused, then reranked. See [concepts](/docs/concepts#embedding-and-hybrid-retrieval). **Tools are called, if precision is needed.** Rather than recalling a number from context, the coach can call the same tools exposed over [MCP](/docs/mcp-tools) to fetch it exactly. This is what makes streak counts and personal records reliable instead of approximately right. **The response streams back**, and insights are extracted from the exchange asynchronously. ## Grounding Two mechanisms keep answers tethered. **Precomputed analytics.** Streaks, personal records, correlations, anomalies and goal progress are computed by background workers, not derived mid-conversation. The coach reads a figure that was calculated properly against your full history, rather than eyeballing whatever slice happened to be in context. This was a real failure mode before it was fixed: reporting a best recovery of 95% when the true maximum was 98, because it was reading the maximum of a 30-day window. **Provenance.** Quantitative results carry `asOf`, sample size, confidence and tier, so the model can weigh a number resting on 90 days differently from one resting on six — and tell you which it is working from. ## Memory The coach extracts durable facts from conversation and keeps them as [learnings](/docs/concepts#learnings): behavioural patterns, preferences, life context, emotional responses, commitments, feedback about its own coaching. Learnings are embedded for semantic retrieval, so the relevant ones surface for the current question rather than all of them arriving at once. They are reinforced when repeated, decay when they stop being mentioned, consolidate when near-duplicates accumulate, and are capped per user. Memory that only grows is memory that becomes noise. The extraction is bidirectional: the coach learns what you tell it about yourself, and it also learns which of its own approaches actually worked on you. ## Tools The coach can call tools mid-answer for precise data: streaks, metric series with statistics, a specific day, the column catalog, WHOOP recovery context, strain balance, correlations, on-demand analysis of any two metrics, bloodwork, genetics. These are the same handlers exposed over MCP, documented in the [tool reference](/docs/mcp-tools). There is one implementation, so an answer computed for the in-app coach and an answer computed for Claude Code come from the same code. ## Threads Conversations persist. You can list, reopen and delete threads, so a line of inquiry from three weeks ago is still there rather than lost to a cleared context. ## Quick actions Preset prompts for the questions people actually ask repeatedly — weekly focus, progress check, what is getting in the way. Faster than retyping, and they route through the same pipeline. ## Limits worth knowing - **It cannot see what you did not log.** A gap in the ledger is a gap in the analysis, and the coach will say so rather than interpolate. - **Early on it has little to work with.** Correlations on a handful of days are noise; the engine declines to report them rather than dressing them up. - **It is not a clinician.** Bloodwork and genetics are interpreted as patterns in your own data against reference ranges, not as diagnosis. ## Related - [Voice agent](/docs/voice) — the same coach, spoken - [Analytics](/docs/analytics) — what the background engines compute - [MCP tool reference](/docs/mcp-tools) — the tools the coach calls --- # Voice agent The coach, spoken. A real-time voice connection rather than record-then-transcribe, so you can interrupt it and it responds like a conversation rather than a walkie-talkie. ## How it works The browser opens a WebSocket directly to the realtime speech model. Before that connection is made, the server mints a short-lived ephemeral credential and builds the system prompt — the same coaching context the text coach uses: your profile, goals, recent data, learnings, streaks and Mission Statement. Two properties follow from that design. Audio never passes through nLight.fit servers, which keeps latency low. And the credential your browser holds is short-lived and scoped to one session, so the long-lived API key stays server-side. ## Modes **Unified** — the default. Talk about anything; the agent moves between coaching and logging as the conversation goes. **Chat** — conversation only. Coaching, questions, working through a problem. Nothing is written to the ledger. **Data entry** — hands-free logging. Say what you did and it fills in the ledger. Useful straight after a workout, or while cooking, when typing into a grid is the wrong interface. **Onboarding** — a guided pass through the profile, roughly fifty fields, conversationally. Considerably faster than the form, and it tends to surface context a form does not, because you explain things rather than selecting them. ## Voice selection The voice is chosen to match the coaching style you set in your profile. An analytical coach and a tough one should not sound the same. ## Learning from speech Voice conversations feed the same [learnings](/docs/concepts#learnings) pipeline as text. Insights are extracted asynchronously after the session, tagged with `voice` as their source, and become available to the text coach immediately. The two are the same coach with the same memory, not two assistants that each know half of it. ## Practical notes - Needs microphone permission and a live connection. - Best in a quiet space — it is a real-time model, and background conversation will be picked up as input. - Data entry confirms what it heard before writing. Check it; speech recognition on numbers is good, not perfect. ## Related - [AI coach](/docs/ai-coach) — the context pipeline behind both surfaces - [Quickstart](/docs/quickstart) — using voice onboarding to fill your profile --- # Mission Statement A goal in one sentence, and the three commitments that would actually get you there. It is deliberately small. Three is enough to be a strategy and few enough to be honest about. ## Why it is separate from your profile The profile describes your situation — age, equipment, injuries, diet, what has failed before. The Mission Statement declares your intent. The platform needs both, and it uses them differently: the profile shapes *how* the coach talks to you, the Mission Statement is what it measures you against. Without it, the coach can tell you what happened. With it, the coach can tell you whether what happened is working. ## What the platform does with it **Tracks progress toward the goal.** Where you are, the rate of change, and — where the goal is quantitative and there is enough history — a projected arrival date with confidence bands rather than a single optimistic number. **Tests whether the commitments are the right ones.** This is the part that is hard to do by hand. Because the platform holds a [correlation graph](/docs/concepts#correlation-graph) across every module, it can ask whether the three behaviours you committed to are in fact the behaviours moving your outcome — or whether something you never listed is doing the work. That is a genuinely uncomfortable feature, and it is the point. Committing to a behaviour that turns out to be uncorrelated with your goal is the most common way a plan quietly fails. **Writes periodic mission analysis.** The summarization pipeline produces a narrative review of goal, commitments and causality, which becomes context for later coaching conversations. ## Writing a good one **The goal should be falsifiable.** "Get in better shape" cannot be measured; "get to 175lbs and run a sub-7 mile" can. **The commitments should be behaviours, not outcomes.** "Weigh 175" is a goal, not a commitment. "Train four times a week", "protein at every meal", "in bed by eleven" are things you actually do, and they are the things a correlation engine can test against an outcome. **They should map to something you track.** A commitment with no corresponding column is invisible to the analysis. If you commit to sleep consistency, make sure sleep consistency is being recorded — by WHOOP, or by a custom column. ## Changing it Change it when it should change. A mission you have outgrown produces analysis about a goal you no longer hold, which is worse than no analysis. The history is retained, so revising is not the same as erasing. ## Related - [AI coach](/docs/ai-coach) — how the mission enters coaching context - [Analytics](/docs/analytics) — goal ETA, causality and the correlation engine - [Concepts](/docs/concepts#behavior-and-outcome) — why behaviours and outcomes are distinguished --- # Analytics Most of the analysis happens before you ask for it. Background workers compute correlations, records, summaries and statistical models on a schedule, and the coach reads the result. That ordering is deliberate. Deriving statistics inside a conversation means deriving them from whatever slice of data happened to be in context, which produces answers that are confidently wrong — a "personal best" that is only the best of the last thirty days. Computing ahead of time against the full history removes that class of error entirely. ## The correlation engine Runs nightly across every module at once: habits, nutrition, wearables, bloodwork, genetics. **Known physiology is tested against your data.** Rather than asserting that sleep drives recovery, the engine checks whether it does *for you*, and labels the result confirmed, contradicted, or confounded. A contradiction is often the more useful finding. **Statistical controls are applied.** Partial correlation controlling for time trend and sleep, so a relationship that is really just "both improved over six months" does not present as causal. Autocorrelation-adjusted significance, because consecutive days are not independent samples. Multiple-testing correction, because testing forty metrics against each other will produce spurious hits by construction. **Results are stored as a graph.** Edges carry the coefficient, the partial coefficient, lag, p-value, sample size, tier and provenance. Depth-2 traversal assembles mechanistic chains — sleep to recovery to next-day strain capacity — instead of asserting them. **Behaviour-to-marker links.** Daily behaviours are tested against bloodwork markers over a biologically plausible exposure window, so a marker that responds over eight weeks is not correlated against yesterday's meals. You can also ask for a pair nobody precomputed. On-demand analysis applies the same guards, and the result is always labelled a hypothesis regardless of how strong the coefficient looks — a single fished pair has no multiple-testing protection. ## Precomputed models The insights engine maintains a set of statistical models, queryable in the app and via `get_insights` over [MCP](/docs/mcp-tools): | Model | What it answers | | --- | --- | | Readiness scoring | How prepared you are today, composited across inputs | | Lead-lag relationships | Which metric moves first when two move together | | Goal ETA | Projected arrival with Monte Carlo confidence bands | | Streak survival | The probability a streak of a given length continues | | Training load | Accumulated load against capacity | | HRV baseline | Your personal baseline and deviation from it | | Momentum | Whether you are building or slipping right now | | Dose-response | Whether more of a behaviour keeps helping, or plateaus | | Changepoints | Dates where a metric's regime actually shifted | | Behavioural archetypes | Recurring patterns in how you operate | ## Facts and records An all-time fact index per metric: first and last recorded day, minimum, maximum, lifetime total where a total is meaningful, current and best streak, and 30- and 90-day rolling averages. Coverage is derived from the metric registry rather than hand-maintained, which is what stops it from silently omitting a group of metrics — an earlier hand-written version omitted every WHOOP column, and the coach reported bests that were nowhere near the true maxima. Direction is declared per metric, because it is not inferable. Lower is better for resting heart rate and mile time. Higher is better for HRV. Weight is deliberately neither, because whether up or down is better depends on your goal, not on the metric. ## Summaries Narrative rollups written by the summarization pipeline: weekly, monthly, quarterly and yearly reviews, lab and methylation interpretations, mission analysis, personal records, and a cross-module correlation narrative. These are embedded alongside individual days, so a question about a period retrieves the period rather than reassembling it from ninety rows. ## Charts Trends, distributions and comparisons over any tracked metric, rendered in the app. Bloodwork markers get sparklines against their reference ranges. ## When things run | Worker | Schedule (UTC) | Does | | --- | --- | --- | | Summaries and insights | 01:00, 05:00, 10:00, 14:00, 17:00, 21:00 | Records, period summaries, mission analysis, meal enrichment, insights, labs and methylation pipelines | | WHOOP sync | 03:30, 13:00, 18:00 | Timezone-aware pull for connected accounts | | Embeddings | 06:00 | Batch-embeds anything flagged during the day | | Learning maintenance | 04:00 | Decay, expiry, consolidation | Days are also embedded inline as you save them; the cron pass is a fallback for anything that failed or was written while the embedding service was unavailable. ## Data quality Analysis is only run on values that can carry it. WHOOP recovery from the calibration period and sleep stages from a night with a significant sensor gap are suppressed for analysis while remaining visible in the ledger exactly as reported. See [quality gating](/docs/concepts#data-quality-gating). ## Related - [Concepts](/docs/concepts) — tiers, provenance, the feature store - [MCP tool reference](/docs/mcp-tools) — querying all of this directly - [Mission Statement](/docs/mission-statement) — goal progress and causality --- # Progress pictures Photos attached to your record, viewable as a series over time. ## Why bother Weight is a lagging and noisy signal. It moves with hydration, sodium, glycogen and time of day, and over a body-composition change it can sit flat for weeks while something real is happening. A photo taken on the same schedule shows a change the scale does not. It is also the metric you are worst at judging in the mirror, because you see yourself daily and the day-to-day delta is zero. Two photos eight weeks apart are a different experience. ## How it works Upload from the app. Each picture is stored against your account with its date, and the set is retrievable as a series. Thumbnails load in the gallery; the full-resolution image is fetched only when you open one, so browsing a long history stays fast. Pictures can be deleted individually. ## Getting comparable photos The value is entirely in comparability, so hold the variables still: - Same time of day, ideally first thing in the morning - Same lighting and same spot - Same distance and camera height - Same poses each time — front, side, back - Similar clothing **Monthly is the right cadence** for most people. Weekly produces noise you will over-read; quarterly loses the intermediate detail that tells you when something changed. ## Privacy Progress pictures are stored against your account and are only ever returned to you. They are not sent to the AI coach, not embedded for semantic search, and not exposed by any tool on the [MCP server](/docs/mcp) — no tool in either catalog reads them. See [security and privacy](/docs/security). ## Related - [The ledger](/docs/ledger) — the rest of what a day holds - [Security and privacy](/docs/security) — what is stored and how --- # WHOOP Connecting WHOOP means recovery, sleep, strain and workouts arrive on their own and sit in the same rows as everything you type. From that point on the correlation engine can test your behaviours against physiological outcomes, which is where most of the interesting findings come from. ## Connecting Authorize once from settings. Standard OAuth — you approve access on WHOOP's side and are returned to the app. If the connection lapses, or WHOOP widens the scopes it needs, reauthorize from the same place. The status of the connection is visible in settings, including when it last synced. Disconnecting stops all syncing. Data already pulled stays in your ledger; it is your record, and removing it would put holes in analysis that has nothing to do with WHOOP. ## What arrives Around forty fields. The [metric reference](/docs/metrics#whoop) lists all of them with types and units. The ones most people show as columns: | Metric | Notes | | --- | --- | | `whoopRecovery` | 0–100%. Green 67+, yellow 34–66, red below 34 | | `whoopHrv` | RMSSD in milliseconds | | `whoopSleep` | Sleep performance — 100% means sleep need was met | | `whoopStrain` | 0–21. Light 0–9, moderate 10–13, hard 14–17, all-out 18+ | | `whoopRhr` | Resting heart rate during sleep | | `_sleepHours` | Derived: deep plus REM plus light | | `whoopSleepConsistency` | How closely bed and wake times matched the recent pattern | | `whoopSkinTemp` | Deviation above baseline often precedes illness by a day or two | Also available: sleep stages and efficiency, disturbances and awake time, sleep need and debt, bedtime and wake time, naps, SpO2 and respiratory rate, calories, average and max heart rate, per-session workout detail with heart-rate zone splits, and time in each of the five zones. You choose which appear as columns. Showing all forty makes the table unusable; the rest remain available to the coach and to analysis whether or not they are displayed. ## Sync schedule Three scheduled pulls a day — 03:30, 13:00 and 18:00 UTC — timezone-aware, so a "day" means your day rather than a UTC day. WHOOP also pushes webhooks when new data is scored. Those are signature-verified and trigger a targeted re-sync, so a recovery score usually appears within minutes of WHOOP computing it rather than waiting for the next scheduled pull. You can also sync manually from settings. ## Quality gating Two WHOOP signals are caveats rather than measurements, and the platform reads them as such. **Calibration.** While WHOOP is still establishing your baseline, the recovery score is provisional. Recovery from those days is excluded from analysis. HRV, resting heart rate and skin temperature are direct measurements and stay trustworthy throughout. **Sleep gaps.** If the strap recorded nothing for a meaningful part of the night — taken off, or a sensor dropout — every sleep-stage total is understated by that much. Those stage values are excluded from analysis. In both cases the raw numbers stay visible in the ledger exactly as WHOOP reported them. Only analysis treats them as missing. The alternative — correlating a partial record as though it were a short night — manufactures relationships that are not real. ## What the coach does with it `get_recovery_context` returns recovery with trends and training recommendations. `analyze_strain_balance` looks at strain against recovery over time and flags overreaching. Both are available in the app and over [MCP](/docs/mcp-tools). More usefully, WHOOP metrics enter the general correlation graph, so questions like "what actually drives my recovery" are answered against your habits, nutrition and bloodwork rather than WHOOP data alone. ## Related - [Metric reference](/docs/metrics#whoop) — every WHOOP field - [Analytics](/docs/analytics) — how the correlation engine uses it - [Concepts](/docs/concepts#data-quality-gating) — quality gating in detail --- # Bloodwork Upload a lab report PDF and its markers become a structured record: values, units, reference ranges, and a trend once you have more than one panel. Bloodwork is where behaviour meets biology. A habit column tells you what you did; a marker tells you what it did to you, on a timescale of weeks rather than days. ## Uploading Upload the PDF from the labs view. Text is extracted from the document **in your browser**, then that text — not the file — is sent for parsing into structured markers. That split is deliberate. Lab reports carry your name, date of birth and provider details in the header, and the file itself never needs to leave your machine for the markers to be read out of it. Extraction handles the common panel formats: metabolic, lipid, hormone, thyroid, inflammatory, vitamins and minerals. You review what was extracted before it is saved, which matters because lab PDFs vary and an occasional marker lands in the wrong row. ## What you get **A panel overview** — every marker with its value, unit, reference range, and whether it sits inside or outside that range. **Trends** — once there are two or more panels, each marker gets a series and a sparkline. This is the point at which bloodwork becomes genuinely useful; a single panel is a snapshot, and a snapshot cannot tell you which direction you are moving. **Statistics** — change since the last panel, change since the first, and rate of change per marker. **Correlations against behaviour.** Daily behaviours are tested against markers over a biologically plausible exposure window, so a marker that responds over eight weeks is correlated against eight weeks of behaviour rather than yesterday's meals. This is what connects "I have been eating better" to "and here is what moved." ## Asking about it `get_lab_data` returns the full panel with precomputed insights, or a single marker with its history. Fuzzy matching handles the usual shorthand — "vit d" finds Vitamin D, "testosterone" finds both total and free. Markers also enter the cross-module correlation graph, so a question about a biomarker can be answered against your habits, nutrition, WHOOP data and genetics together rather than in isolation. ## Genetics interaction If you have uploaded a [methylation profile](/docs/methylation), the platform cross-links the two: your genetic variants predict which markers are worth watching, and those predictions get tested against your actual panels. A variant that suggests you should watch homocysteine becomes a hypothesis, and your bloodwork either supports it or does not. ## Cadence Two to four panels a year is typical, and enough to build a trend. More often than every eight weeks is usually not informative — most markers move slower than that, and you end up reading noise as signal. ## Limits **This is not medical advice.** The platform reports your values against reference ranges and against your own history, and identifies patterns. It does not diagnose, and it is not a substitute for the clinician who ordered the panel. **Extraction can be wrong.** Review what was parsed. The PDFs are inconsistent and the extraction is good rather than infallible. ## Related - [Methylation](/docs/methylation) — genetics as a moderator for these markers - [Analytics](/docs/analytics) — how behaviour-to-marker correlation works - [Security and privacy](/docs/security) — how health data is stored --- # Methylation genetics A one-time upload of a methylation genetic test: genes like MTHFR, COMT, MTR, MTRR and AHCY, and which variants you carry. ## Genetics is a moderator, not a metric This is the important framing, and it is why methylation is handled differently from everything else in the platform. Your genotype does not change. There is no trend to plot and no streak to maintain. What it does is **explain mechanisms and predict which things are worth watching** — why you respond the way you do to caffeine, stress, B-vitamins or a particular supplement, and which biomarkers your variants suggest keeping an eye on. So it enters analysis as a **prior**, not as a variable. It generates hypotheses; your bloodwork and your daily data test them. ## Uploading Upload the PDF from the methylation view. As with [bloodwork](/docs/labs), text extraction happens in your browser and only the extracted text is sent for parsing into structured gene and variant records. You do this once, ever. ## What you get **A pathway overview.** Your variants organized by the pathway they sit in, rather than as an alphabetical gene list — which is how they actually interact. **Clusters.** Where several variants in the same pathway compound each other. A single heterozygous variant is usually unremarkable; three in the same pathway is a different statement. **Watch markers.** Bloodwork markers your genotype suggests monitoring, cross-linked to your actual [lab results](/docs/labs) where you have them. This is the most directly actionable output: a specific, testable list rather than general interest. **Genetic priors.** Expectations about how you are likely to respond to particular inputs, which the correlation engine then treats as hypotheses to test rather than conclusions to report. **Validation.** Where your own data supports or contradicts what the genotype predicted. A contradiction is not an error — it means something downstream is compensating, and that is worth knowing. ## Asking about it `get_methylation_data` returns the full pathway overview with watch markers and priors, or a single gene. Ask the coach about a gene, about why you respond a certain way to something, or about which bloodwork your genes suggest tracking. ## Limits worth being clear about **Genotype is not destiny.** A variant describes a tendency in enzyme activity, not an outcome. Expression, diet, environment and everything else you do sit between the gene and the result. That is the whole reason the platform treats it as a prior to be tested rather than a fact to be acted on. **This is not clinical genetic testing.** Consumer methylation panels are not diagnostic instruments and this platform is not a clinician. Nothing here should drive a medical decision without a professional involved. **Be skeptical of confident genetic advice generally.** The field is noisier than its marketing suggests. The platform's approach — generate a hypothesis, then check it against your own bloodwork and behaviour — is deliberately more conservative than telling you what your genes mean. ## Related - [Bloodwork](/docs/labs) — the markers these predictions get tested against - [Analytics](/docs/analytics) — how priors enter the correlation engine - [Security and privacy](/docs/security) — how genetic data is stored --- # MCP server nLight.fit runs a remote [Model Context Protocol](https://modelcontextprotocol.io) server at `POST https://nlight.fit/api/mcp`. It lets an AI client you already use — Claude Desktop, Claude Code, Cursor, or anything else that speaks MCP — query your own tracked data, WHOOP metrics, bloodwork and correlations directly. The tools are the same handlers the in-app coach uses. There is no second implementation, so an answer you get in Cursor is computed exactly the way the answer in the app is. ## What you can reach Sixteen tools, in two groups. **Curated tools** are the ten the in-app coach also calls. They return a conclusion the analysis pipeline already reached: a streak, a metric series with statistics, a correlation the nightly engine judged significant. They are small, fast, and hard to misuse. **Warehouse tools** are six that exist only over MCP. They return the underlying evidence instead of a verdict — the raw daily feature matrix, the correlation graph as structured edges, the precomputed statistical models — so a client can test a hypothesis the engine never considered. They are deliberately not offered to the in-app coach, because a 365-day by 40-metric matrix dropped into a chat context produces a worse answer than the single precomputed edge it already had. The [tool reference](/docs/mcp-tools) lists all sixteen with full input schemas. ## Read-only, and why Every exposed tool is a query. Nothing reachable over MCP can change your data. That is enforced in three places rather than assumed. The endpoint requires the `read` scope. Tools are published from an explicit allowlist, not from "everything the coach can call", so a write tool added for the in-app coach cannot become publicly reachable by merging. And every tool descriptor carries `annotations.readOnlyHint: true`, which lets a client run any of them without prompting you for confirmation. Write tools are a separate phase. They will require a token carrying the `write` scope, which the auth layer already understands but nothing currently grants over this endpoint. ## Transport Stateless JSON-RPC 2.0 over HTTP POST. One message per request, no session, no handshake required, no long-lived stream. This matters for a practical reason: the older session-based MCP transport needed a connection pinned to a single server instance, which a serverless platform cannot promise. Protocol revision `2026-07-28` removed the handshake and made every message self-contained, which is what allows this to be an ordinary Vercel function rather than a separate always-on service. Older clients that still open with `initialize` are handled too. Which era is in play is decided by the `MCP-Protocol-Version` header. See [errors and limits](/docs/errors) for the supported list. Two consequences worth knowing: - `GET` returns `405`. There is no event stream to connect to. - JSON-RPC batching is rejected. Send one message per request. ## Supported methods | Method | Purpose | | --- | --- | | `server/discover` | Stateless capability discovery. Returns protocol version, capabilities and server info. | | `initialize` | The legacy handshake. Still answered for older clients; negotiates the highest revision you both speak. | | `tools/list` | The tool catalog. Carries a one-hour cache hint scoped `private`. | | `tools/call` | Invoke a tool. | | `ping` | Health check. Returns an empty object. | `tools/list` is marked `cacheScope: private` deliberately. The catalog is currently identical for every user, but tool descriptions cite your custom columns by name, so a shared cache must never serve one account's vocabulary to another. There are no MCP resources or prompts today — only tools. ## A minimal call ```bash curl -sX POST https://nlight.fit/api/mcp \ -H "Authorization: Bearer $NLIGHT_TOKEN" \ -H "Content-Type: application/json" \ -H "MCP-Protocol-Version: 2026-07-28" \ -H "Mcp-Method: tools/list" \ -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}' ``` From revision `2026-07-28` onward the `Mcp-Method` header is required, and `Mcp-Name` is required for messages that address something. The server checks both against the request body and rejects disagreement, so an intermediary cannot route on one operation while the server performs another. ## Browser requests are refused Any request carrying an `Origin` header is rejected with `403` unless that origin is explicitly allowlisted by the server operator. This is real access control rather than defensive tidying. The site applies a permissive `Access-Control-Allow-Origin: *` across `/api/*`, which is fine for endpoints that need a browser but not for one that returns a complete medical record. Genuine MCP clients are servers and send no `Origin` at all, so refusing every request that carries one costs nothing and closes both the CORS gap and the DNS-rebinding case the transport specification warns about. ## Getting connected 1. Mint an API token — see [token management](/docs/token-scripts). 2. Add the server to your client — see [client setup](/docs/clients). 3. Ask your client to list tools, and confirm you get sixteen back. ## Related - [MCP tool reference](/docs/mcp-tools) — all sixteen tools with input schemas - [Authentication](/docs/authentication) — how API tokens and scopes work - [Errors and limits](/docs/errors) — status codes, rate limits, protocol versions --- # MCP tool reference The server exposes **16 tools**, all read-only. Every tool carries `annotations.readOnlyHint: true`, so a client may run any of them without asking for confirmation. This page is generated from the running tool catalog. It is the same list `tools/list` returns. | Tool | Catalog | Purpose | | --- | --- | --- | | [`get_streak`](#get-streak) | Curated | Get current and historical streak data for any tracked habit or metric. | | [`get_metric_data`](#get-metric-data) | Curated | Retrieve historical data for any metric with statistics. | | [`get_day_details`](#get-day-details) | Curated | Get all tracked data for a specific date. | | [`list_columns`](#list-columns) | Curated | Get list of all tracked metrics/columns for this user with their descriptions. | | [`get_recovery_context`](#get-recovery-context) | Curated | Get WHOOP recovery metrics with context and trends. | | [`analyze_strain_balance`](#analyze-strain-balance) | Curated | Analyze strain-to-recovery balance over time. | | [`get_correlations`](#get-correlations) | Curated | THE single tool for any 'what affects X', 'why is X changing', 'what's connected to X', or pattern/correlation question - across EVERY data source (WHOOP recovery/HRV/sleep/strain, nutrition, habits, weight, custom metrics, bloodwork labs, AND methylation genetics). | | [`analyze_relationship`](#analyze-relationship) | Curated | Compute a rigorous correlation between ANY TWO metrics ON DEMAND, for pairs the nightly engine didn't precompute (get_correlations only has a fixed catalog). | | [`get_lab_data`](#get-lab-data) | Curated | Get the user's bloodwork lab results (biomarkers like testosterone, vitamin D, ApoB, triglycerides, TSH, ferritin, hs-CRP, cholesterol). | | [`get_methylation_data`](#get-methylation-data) | Curated | Get the user's methylation genetic test results (a one-time, STABLE genetic profile: genes like MTHFR, COMT, MTR, MTRR, AHCY and their variants). | | [`get_daily_features`](#get-daily-features) | Warehouse | Retrieve the raw daily feature matrix: each metric in this user's retrieval catalog (call list_metrics for the exact set - it includes their custom columns) as a normalized numeric series plus its z-score against a trailing 30-day baseline. | | [`list_metrics`](#list-metrics) | Warehouse | List this user's retrieval catalog - every metric get_daily_features can return: ids, labels, units, domains, and whether each is a behavior or an outcome. | | [`get_correlation_edges`](#get-correlation-edges) | Warehouse | Query the correlation graph directly as structured edges, with the statistics attached (r, partial r, lag, p-value, sample size, tier, and provenance). | | [`get_user_facts`](#get-user-facts) | Warehouse | All-time aggregates per metric: first and last recorded day, min, max, total, current and best streak, and 30/90-day rolling averages. | | [`get_insights`](#get-insights) | Warehouse | Precomputed statistical models the nightly engine maintains: readiness scoring, lead-lag relationships, goal ETA with Monte Carlo confidence bands, streak survival curves, training load, HRV baseline, momentum, dose-response curves, changepoint detection and behavioral archetypes. | | [`get_summaries`](#get-summaries) | Warehouse | Narrative rollups written by the summarization pipeline: period reviews, lab and methylation interpretations, mission analysis, personal records and the cross-module correlation narrative. | ## Curated tools These are the same tools the in-app coach uses. They return a conclusion the analysis pipeline already reached, which makes them cheap to call and easy to reason about. ### get_streak Get current and historical streak data for any tracked habit or metric. Use when user asks about streaks, consecutive days, or 'how many days in a row' for ANY metric - especially custom columns like No Zyn, No Weed, No Alcohol, GMAD, etc. ALWAYS use this for custom/non-standard habits. Supports fuzzy matching: 'no alcohol' finds 'No Alch', 'nicotine' finds 'No Zyn', etc. | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `metric` | `string` | Yes | The metric to check. Supports: (1) Standard habits: 'vitamins', 'wake7am', 'reading', 'exercise', (2) Custom columns by ID, label, or description keywords: 'noZyn', 'No Weed', 'alcohol', 'nicotine', 'gmad'. Fuzzy matching handles abbreviations and synonyms. | | `include_history` | `boolean` | No | If true, include when streak started and previous streak segments. Default: false | ```json { "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "get_streak", "arguments": { "metric": "" } } } ``` ### get_metric_data Retrieve historical data for any metric with statistics. Use when user asks about specific values, trends, averages, totals, or comparisons over time. Supports all standard and custom columns. | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `metric` | `string` | Yes | The metric to retrieve: 'weight', 'pushUps', 'sitUps', 'healthyEating', 'workoutSession', or any custom column ID/label | | `time_range` | `today` \| `yesterday` \| `week` \| `month` \| `quarter` \| `year` \| `all` | No | Time period to retrieve. Default: 'month' | | `include_stats` | `boolean` | No | Include computed statistics (avg, min, max, trend). Default: true | ```json { "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "get_metric_data", "arguments": { "metric": "" } } } ``` ### get_day_details Get all tracked data for a specific date. Use when user asks about a particular day, what they did on a date, or needs to verify what was logged. | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `date` | `string` | Yes | Date in YYYY-MM-DD format, or relative: 'today', 'yesterday' | ```json { "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "get_day_details", "arguments": { "date": "" } } } ``` ### list_columns Get list of all tracked metrics/columns for this user with their descriptions. Use when unsure what columns exist, need to find the correct column ID, or user asks what they can track. Custom columns include descriptions explaining what they mean (e.g., 'No Alch' description: 'No alcohol consumption'). | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `include_stats` | `boolean` | No | Include basic stats per column (days tracked, last value). Default: false | ```json { "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "list_columns", "arguments": {} } } ``` ### get_recovery_context Get WHOOP recovery metrics with context and trends. Use when user asks about their recovery readiness, HRV, sleep quality, strain, or whether they're recovered enough to work out. Provides recovery zone (green/yellow/red), recent trends, and training recommendations. | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `days_back` | `number` | No | Number of days to analyze for trends. Default: 7 | | `include_recommendations` | `boolean` | No | Include training recommendations based on recovery state. Default: true | ```json { "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "get_recovery_context", "arguments": {} } } ``` ### analyze_strain_balance Analyze strain-to-recovery balance over time. Use when user asks about overtraining, training load, whether they're pushing too hard, or optimal training intensity. Shows strain vs recovery relationship and identifies imbalances. | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `time_range` | `week` \| `month` \| `quarter` | No | Time period to analyze. Default: 'week' | ```json { "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "analyze_strain_balance", "arguments": {} } } ``` ### get_correlations THE single tool for any 'what affects X', 'why is X changing', 'what's connected to X', or pattern/correlation question - across EVERY data source (WHOOP recovery/HRV/sleep/strain, nutrition, habits, weight, custom metrics, bloodwork labs, AND methylation genetics). Returns known physiology tested on THEIR data (confirmed/contradicted/confounded), discovered patterns, behavior->lab links, and genetics-informed expectations - all with partial-correlation controls and multiple-testing correction, so the numbers are authoritative (don't recompute from raw rows). Use it for WHOOP-specific pattern questions too. When a metric is given, also returns a GRAPH view: the direct edges touching that metric and (depth 2) one-hop mechanistic chains (e.g. sleep -> recovery -> next-day strain capacity). | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `metric` | `string` | No | Optional: filter findings to those involving this metric (fuzzy matched), e.g. 'recovery', 'triglycerides', 'meal quality', 'No Alcohol'. Omit for the full cross-module picture. | | `status_filter` | `all` \| `confirmed` \| `contradicted` \| `robust` | No | Optional: 'confirmed' for validated physiology, 'contradicted' for surprising deviations or confounded links worth investigating, 'robust' for only the highest-confidence patterns (survived confounder controls + multiple-testing correction). Default: 'all' | | `depth` | `1` \| `2` | No | Graph traversal depth when a metric is provided. 1 = direct edges touching the metric (default). 2 = also return one-hop chains (metric -> intermediate -> downstream) for mechanistic storytelling. | ```json { "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "get_correlations", "arguments": {} } } ``` ### analyze_relationship Compute a rigorous correlation between ANY TWO metrics ON DEMAND, for pairs the nightly engine didn't precompute (get_correlations only has a fixed catalog). Use when the user asks whether two specific things relate that aren't already in get_correlations - e.g. 'does my sauna use affect my HRV?', 'is there a link between my late dinners and deep sleep?', 'does creatine correlate with my push-up count?'. Works for daily metric x daily metric (habits, WHOOP, fitness, nutrition, custom columns) and daily behavior x lab marker. Applies the SAME statistical guards as the nightly engine (partial correlation controlling for time trend + sleep, autocorrelation-adjusted significance) so it never just eyeballs raw data. Results on a single fished pair are hypotheses, never 'robust'. | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `metric_a` | `string` | Yes | First metric (the behavior/driver), fuzzy matched. E.g. 'sauna', 'late dinner', 'creatine', 'sleep hours', 'meal quality', 'vitamins'. | | `metric_b` | `string` | Yes | Second metric (the outcome), fuzzy matched. Can be a daily metric ('HRV', 'recovery', 'push-ups', 'deep sleep') OR a bloodwork marker ('triglycerides', 'vitamin d', 'testosterone'). | | `lag` | `0` \| `1` | No | Optional day lag for daily x daily: 0 = same day (default search tries both 0 and 1 and reports the stronger). Ignored for lab-marker outcomes. | | `window` | `integer` | No | Optional lookback window in days (e.g. 90). Omit to use up to a year of data. | ```json { "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "analyze_relationship", "arguments": { "metric_a": "", "metric_b": "" } } } ``` ### get_lab_data Get the user's bloodwork lab results (biomarkers like testosterone, vitamin D, ApoB, triglycerides, TSH, ferritin, hs-CRP, cholesterol). Use when the user asks about their labs, bloodwork, a specific biomarker's value or trend, or hormone/metabolic/cardiovascular health backed by blood tests. Supports fuzzy matching: 'vit d' finds Vitamin D, 'testosterone' finds Total/Free Testosterone. Without a marker, returns a panel overview with precomputed insights. | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `marker` | `string` | No | Biomarker to look up by name or key, e.g. 'vitamin d', 'apob', 'triglycerides', 'free testosterone'. Omit for a full panel overview. | | `include_history` | `boolean` | No | Include every historical reading (date + value), not just latest and stats. Default: true when a marker is specified. | ```json { "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "get_lab_data", "arguments": {} } } ``` ### get_methylation_data Get the user's methylation genetic test results (a one-time, STABLE genetic profile: genes like MTHFR, COMT, MTR, MTRR, AHCY and their variants). Use when the user asks about their genetics, methylation, a specific gene, why they respond a certain way to caffeine/stress/supplements/B-vitamins, or which bloodwork their genes suggest watching. Genetics is a MODERATOR, not a daily metric - it explains mechanisms and predicts which labs/behaviors matter, and generates hypotheses that get tested in get_correlations. Without a gene, returns the full pathway overview plus genetically-predicted watch-markers (cross-linked to their labs) and genetic priors. | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `gene` | `string` | No | Optional gene symbol to look up, e.g. 'MTHFR', 'COMT', 'MTR'. Omit for the full pathway overview. | ```json { "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "get_methylation_data", "arguments": {} } } ``` ## Warehouse tools These exist only over MCP. They return the underlying evidence rather than a verdict, so a client can test a hypothesis the nightly engine never considered. Results are larger; narrow the window or the metric list when you can. ### get_daily_features Retrieve the raw daily feature matrix: each metric in this user's retrieval catalog (call list_metrics for the exact set - it includes their custom columns) as a normalized numeric series plus its z-score against a trailing 30-day baseline. This is the analysis primitive - use it to test your OWN hypotheses (custom lag windows, three-way interactions, subgroup splits, regime changes) rather than reading conclusions someone else drew. Returns columnar data: a date axis plus one array per metric, aligned by index, with null for days that have no value. For a precomputed verdict on whether a relationship is statistically real, use get_correlations instead. | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `from` | `string` | No | Start date YYYY-MM-DD (inclusive). Defaults to 90 days before `to`. | | `to` | `string` | No | End date YYYY-MM-DD (inclusive). Defaults to today. | | `metrics` | `string[]` | No | Metric ids to return, from list_metrics. Omit for all metrics, which is only allowed for windows of 180 days or less. | | `include_z` | `boolean` | No | Include z-scores alongside raw values. Default: true. | ```json { "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "get_daily_features", "arguments": {} } } ``` ### list_metrics List this user's retrieval catalog - every metric get_daily_features can return: ids, labels, units, domains, and whether each is a behavior or an outcome. Includes their own custom columns under domain 'custom', so the set differs per user. Call this before get_daily_features or analyze_relationship to learn the exact ids. Distinct from list_columns, which describes the user-facing tracking table; this describes the analytical schema and includes derived metrics such as _exercise and _sleepHours that have no column of their own. Note that the nightly correlation scan runs over a deliberately narrower curated subset, so a metric listed here may have no precomputed findings in get_correlations. | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `domain` | `string` | No | Filter by domain. The catalog defines exactly these: 'custom', 'fitness', 'habits', 'nutrition', 'whoop'. | | `role` | `behavior` \| `outcome` | No | Filter by whether the metric is something the user does or something that happens to them. | ```json { "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "list_metrics", "arguments": {} } } ``` ### get_correlation_edges Query the correlation graph directly as structured edges, with the statistics attached (r, partial r, lag, p-value, sample size, tier, and provenance). Use this when you want to filter, sort or traverse relationships yourself - for example every edge above a given effect size, or the mechanistic chain connecting two metrics. get_correlations returns the same underlying analysis as prose aimed at a person; this returns rows aimed at a program. | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `metric` | `string` | No | Focus the graph on one metric (fuzzy matched on id and label). Omit to return all edges. | | `depth` | `1` \| `2` | No | With `metric`: 1 returns direct edges, 2 also returns one-hop chains through an intermediate metric. Default: 1. | | `min_abs_r` | `number` | No | Drop edges whose absolute correlation is below this, 0-1. | | `tier` | `string` | No | Keep only edges of this confidence tier, e.g. 'confirmed', 'probable', 'hypothesis'. | | `limit` | `number` | No | Maximum edges to return, strongest first. Default: 100. | ```json { "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "get_correlation_edges", "arguments": {} } } ``` ### get_user_facts All-time aggregates per metric: first and last recorded day, min, max, total, current and best streak, and 30/90-day rolling averages. Use this to ground a claim about a personal record or a lifetime trend without pulling the full history. Much cheaper than get_daily_features when the question is about extremes or streaks rather than day-to-day shape. | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `metrics` | `string[]` | No | Restrict to these metric ids. Omit for all. | ```json { "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "get_user_facts", "arguments": {} } } ``` ### get_insights Precomputed statistical models the nightly engine maintains: readiness scoring, lead-lag relationships, goal ETA with Monte Carlo confidence bands, streak survival curves, training load, HRV baseline, momentum, dose-response curves, changepoint detection and behavioral archetypes. Ask for specific sections rather than the whole document - it is large. | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `sections` | `string[]` | No | Sections to return, e.g. ['goalEta','changepoints']. Omit to receive the list of available sections plus a size estimate for each, which is the cheapest way to decide what to ask for. | ```json { "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "get_insights", "arguments": {} } } ``` ### get_summaries Narrative rollups written by the summarization pipeline: period reviews, lab and methylation interpretations, mission analysis, personal records and the cross-module correlation narrative. Useful for recovering what was happening in a period, or the qualitative context around a numeric change you found in the feature matrix. | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `type` | `string` | No | Summary type, e.g. 'weekly_summary', 'monthly_summary', 'unified_correlations', 'lab_insights'. Call without this argument first to get the exact types that exist for this user, with counts. | | `from` | `string` | No | Earliest period key to include, e.g. 2026-W03 or 2026-01. | | `to` | `string` | No | Latest period key to include. | | `limit` | `number` | No | Maximum summaries to return, most recent first. Default: 12. | ```json { "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "get_summaries", "arguments": {} } } ``` --- # Authentication Machine callers authenticate with a Bearer token: ```http Authorization: Bearer rt_ro_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx ``` There is no OAuth flow on this endpoint. Tokens are minted deliberately, one per client, and revoked individually. ## Token format ``` rt__<43 characters base64url> ``` - `rt` identifies it as an nLight.fit API token. A credential without this prefix is never looked up in the database, so presenting an unrelated key costs nothing to reject. - `` is `ro` or `rw`. It is **cosmetic** — it exists so you can recognise a token you find in a config file. Authorization always reads scopes from the database, never from the token string, which the holder controls. - The secret is 32 bytes of CSPRNG entropy, base64url-encoded to 43 characters with no padding. ## Scopes | Scope | Grants | | --- | --- | | `read` | Every non-mutating tool. Required by every MCP method today, including `initialize` and `ping`. | | `write` | Anything that changes stored data. Defined and enforced, but nothing over MCP requires it yet. | Scopes are **explicit and non-cumulative**. A `write` token does not automatically get `read`. That is deliberate: granting one capability should never silently grant another. If you want a token that can do both, ask for both. A token that is valid but lacks the scope a request needs gets `403`, not `401` — you are who you say you are, you simply may not do this. Telling the client to re-authenticate would send it round a loop it cannot win. ## What is stored Only `sha256(token)`. The plaintext is shown exactly once, at creation, and never again. A leaked database dump therefore yields no usable credentials, and a lost token cannot be recovered — revoke it and mint another. Each token row also holds a name you chose, a 12-character display prefix so you can tell two tokens apart in a list, the granted scopes, creation time, last-used time, and revocation state. ### Why SHA-256 and not bcrypt bcrypt exists to slow down guessing of low-entropy human passwords. These tokens carry 256 bits of entropy, so brute force is not in the threat model, and a deliberately slow hash would add roughly 250ms to every single MCP call. Account passwords still use bcrypt, which is the right choice for what those protect. ## Expiry and revocation Tokens do not expire by default. That is only safe because revocation is immediate: every call looks the token up in the database, so a revoked token stops working on the next request rather than whenever a signature would have lapsed. Revocation is idempotent. Revoking an already-revoked token reports success without changing the original revocation timestamp, so a panicked second attempt is not an error. ## Why not session tokens Session tokens are stateless and signed, which means they cannot be revoked — a stateless token stays valid until it expires, with no server-side blocklist. That trade-off is acceptable for a 24-hour browser session. It is not acceptable for a credential that lives indefinitely inside a third party's cloud and unlocks a complete medical history. If one leaks it has to die immediately, which means the server must look it up. So API tokens are opaque random strings backed by a database row, not signed self-describing tokens. ## Session tokens are accepted, read-only A browser session token presented to `/api/mcp` will authenticate, but it is treated as carrying `read` and nothing else. This exists as a convenience for local testing and for any in-app "verify my connection" affordance. Anything that mutates data over MCP must present a purpose-built token, so a stolen browser session can never be replayed into a write. ## What is not accepted The compatibility mode that lets some older in-app requests identify a user by a plain `userId` in the request body is never consulted here. That fallback exists only to keep already-open browser tabs working through a rollout. This surface is new, has no legacy clients, and is reachable from the public internet — honouring a bare `userId` would hand every account to anyone who can type a 24-character id. ## Related - [Token management](/docs/token-scripts) — minting, listing and revoking tokens - [Client setup](/docs/clients) — wiring a token into Claude or Cursor - [Errors and limits](/docs/errors) — the full status code table --- # Client setup Every client needs the same three things: the endpoint `https://nlight.fit/api/mcp`, the HTTP transport, and an `Authorization: Bearer` header carrying your token. Mint a token first — see [token management](/docs/token-scripts). ## Claude Code ```bash claude mcp add --transport http nlight https://nlight.fit/api/mcp \ --header "Authorization: Bearer rt_ro_your_token_here" ``` Confirm it connected: ```bash claude mcp list ``` ## Claude Desktop Desktop's config file understands stdio servers only. It has no field for a remote URL, and it does not ignore one it cannot parse — it rewrites the file without that entry, taking every other server in `mcpServers` with it. Bridge the endpoint through `mcp-remote` instead, which Desktop launches as an ordinary local process. Open Settings → Developer → Edit Config and add: ```json { "mcpServers": { "nlight": { "command": "npx", "args": ["-y", "mcp-remote", "https://nlight.fit/api/mcp", "--header", "Authorization:${AUTH_HEADER}"], "env": { "AUTH_HEADER": "Bearer rt_ro_your_token_here" } } } } ``` Restart Claude Desktop. The tools appear under the server name `nlight`. Two details in that snippet are load-bearing. The credential sits in `env` because Desktop mangles arguments containing spaces, and `Authorization: Bearer rt_ro_...` has two. And there is no space after `Authorization:` — `mcp-remote` splits the header at the first colon, so the space that belongs to `Bearer ` lives in the environment variable, where nothing will strip it. Settings → Connectors → Add custom connector will not work for this server. That flow authenticates over OAuth: it has no field for a bearer token, so it looks for an authorization server, finds none, and stops at *you started connecting but didn't finish*. Use the config file. ## Cursor Add the server to `.cursor/mcp.json` in a project, or to `~/.cursor/mcp.json` for every project: ```json { "mcpServers": { "nlight": { "url": "https://nlight.fit/api/mcp", "headers": { "Authorization": "Bearer rt_ro_your_token_here" } } } } ``` ## A custom client The endpoint is ordinary HTTP, so any language works. Nothing MCP-specific is required beyond setting the headers correctly. ```bash curl -sX POST https://nlight.fit/api/mcp \ -H "Authorization: Bearer $NLIGHT_TOKEN" \ -H "Content-Type: application/json" \ -H "MCP-Protocol-Version: 2026-07-28" \ -H "Mcp-Method: tools/call" \ -H "Mcp-Name: get_streak" \ -d '{ "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "get_streak", "arguments": { "metric": "vitamins" } } }' ``` Three rules to build against: - Send one JSON-RPC message per request. Arrays are rejected. - From protocol revision `2026-07-28`, `Mcp-Method` is required and must match the body. `Mcp-Name` is required for `tools/call` and must match `params.name`. - Do not send an `Origin` header. Requests carrying one are refused as browser traffic. ## Verifying the connection Ask your client to list the available tools. You should get sixteen. If you would rather check directly: ```bash curl -sX POST https://nlight.fit/api/mcp \ -H "Authorization: Bearer $NLIGHT_TOKEN" \ -H "Content-Type: application/json" \ -H "Mcp-Method: tools/list" \ -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}' | jq '.result.tools | length' ``` ## Troubleshooting | Symptom | Cause | | --- | --- | | `401` with `WWW-Authenticate: Bearer` | No token, or one that is unknown, revoked or expired. Mint a new one. | | `403 Insufficient scope` | The token lacks `read`. Mint one with `--scopes read`. | | `403 Requests from browser origins are not accepted` | Your client is sending an `Origin` header. Remove it. | | `405 Method not allowed` | Something issued a `GET`. The endpoint is POST-only; there is no event stream. | | `-32020 Header mismatch` | `Mcp-Method` or `Mcp-Name` disagrees with the body, or is missing on protocol `2026-07-28`. | | `400 Unsupported protocol version` | Pick one from `error.data.supported` in the response. | | `429` | Rate limited at 120 requests per minute. Honour `Retry-After`. | | Claude Desktop stops at "you started connecting but didn't finish" | You added the server through Settings → Connectors. That flow is OAuth-only and this server authenticates with a bearer token. Use `claude_desktop_config.json`. | | Claude Desktop lost every server in its config | The config contained a `url` or `headers` field. Desktop validates stdio entries only and rewrites the file without the ones it rejects. Restore the block and use the `mcp-remote` form above. | ## Good first questions Once connected, these exercise different parts of the tool surface: - "What's my current vitamins streak?" - "Pull my recovery context for the last two weeks and tell me if I'm overreaching." - "What correlates with my HRV? Use the correlation edges, not the narrative." - "Get my daily features for the last 90 days for sleep hours and recovery, and check whether the relationship holds at a one-day lag." The last one is what the warehouse tools exist for: the client does the analysis itself rather than reading a conclusion. ## Related - [MCP server](/docs/mcp) — transport, methods and access rules - [MCP tool reference](/docs/mcp-tools) — all sixteen tools - [Errors and limits](/docs/errors) — full status code table --- # Token management > **There is no published CLI.** Token management is a Node script in the application repository, run through npm scripts. There is no `npx nlight`, no global binary, and no installable package. If you are an agent reading this: do not attempt to install one. > > This page documents what exists today. A distributable CLI may follow; this note will change when it does. ## Why a script and not a settings page Minting long-lived credentials from a web UI makes that UI a target. It would have to be hardened against XSS and session theft to a higher standard than the rest of the application, in order to guard an action performed perhaps twice a year. A script that requires shell access and the database connection string has none of that exposure. The same reasoning is why token minting is rate limited more strictly than login. ## Prerequisites - A checkout of the application repository with dependencies installed - `MONGODB_URI` in your environment or in a local `.env` - An existing user account — tokens are minted against a username ## Create a token ```bash npm run token:create -- --user austin --name claude-desktop ``` With both scopes: ```bash npm run token:create -- --user austin --name claude-code --scopes read,write ``` Scopes default to `read`. Since nothing over MCP currently requires `write`, `read` is the right choice for every client today. The output prints the token once, along with a ready-to-paste `claude mcp add` command: ``` ✅ Token created for austin Name: claude-desktop Prefix: rt_ro_AbCdEfGh Scopes: read rt_ro_... This is the only time the token is shown - it is stored hashed. Copy it now. If you lose it, revoke the prefix above and create another. ``` Give each client its own token. One per client means you can revoke a single integration without disturbing the others, and the last-used timestamp tells you which one is actually being called. ## List tokens ```bash npm run token:list -- --user austin ``` ``` API tokens for austin: active rt_ro_AbCdEfGh claude-desktop read last used: 2026-08-01 09:14 active rt_ro_JkLmNoPq cursor read last used: never revoked rt_rw_RsTuVwXy old-experiment read,write last used: 2026-06-02 11:40 ``` Hashes are never returned. The prefix is the handle you use to revoke. ## Revoke a token By prefix: ```bash npm run token:revoke -- --prefix rt_ro_AbCdEfGh ``` Everything a user holds: ```bash npm run token:revoke -- --user austin --all ``` Revocation takes effect on the next request. There is no cache to wait out. ## Command reference | Script | Flags | Purpose | | --- | --- | --- | | `token:create` | `--user ` `--name