# 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 through the day 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 **Open signup.** Anyone can create a free account — no card, no waitlist, no approval step. Registration takes a username and a password and signs you straight in. **Two plans.** Free covers the whole tracking product and gives you the coach on the same frontier model, thinking over your last 60 days. Member is $50/year, tax included, and is the tier that funds frontier-model intelligence on your full record — no history horizon, highest reasoning, mission analysis, expanded voice. What that actually changes, with examples, is on [membership](/docs/membership). Upgrade from [Pricing](/#pricing) or from Settings in the app. Checkout is attached to your signed-in account; a public Stripe link is never used. Create an account at [nlight.fit/app](/app). ## 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. - **Background cron workers**, split by cost, that compute summaries, personal records, correlations and insights ahead of time, so the coach reads a precomputed answer instead of deriving one mid-conversation. See [when things run](/docs/analytics#when-things-run). 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 - [Membership](/docs/membership) — why $50/year, and what higher reasoning changes - [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 - [Docs assistant](/docs/docs-assistant) — the public overlay that will walk you here --- # Membership nLight.fit's paid plan is **$50 per year**, tax-inclusive. That is not a cheaper model with a prettier wrapper. Everyone talks to the same coach (`grok-4.6`). What $50 buys is **how hard the coach is allowed to think**, and **how much of your own record it is allowed to think over**. The short version: free coaching answers from the last couple of months with a high reasoning ceiling. Member coaching answers from the whole history at maximum reasoning depth, with hybrid retrieval, an extra tool round, Mission Analysis, and a larger monthly conversation budget. ## What $50 a year buys | Capability | Free | Member | | --- | --- | --- | | Tracking / WHOOP / CSV | Full | Full | | Insights / charts | Full | Full | | Coach model | `grok-4.6` | `grok-4.6` | | Reasoning | up to `high` | up to `xhigh` | | History | ~60 days | full record | | Retrieval | vector | hybrid + rerank | | Tool rounds | 2 | 3 | | Coach messages | ~15/mo | ~40/mo | | Mission Analysis | no | yes | | Voice | ~10 min/mo | ~60 min/mo | The tracking half of the product is not the upgrade. The upgrade is the coach being allowed to reason at full depth over the record you actually have. See [AI coach](/docs/ai-coach) for how a question is classified, and [concepts](/docs/concepts#embedding-and-hybrid-retrieval) for what hybrid retrieval does. ## Why higher reasoning is the product A simple lookup does not need deep thought. "What did I weigh on Tuesday" is a fetch. The coach already routes that kind of question to a low-effort pass so it comes back in a couple of seconds. The questions people actually pay a coach for are the other kind: - Why has recovery been sliding since April. - Is late training wrecking sleep, or is that a story I told myself. - Did the creatine stretch in February change anything that is still visible now. - Are the three commitments on my Mission Statement actually moving the goal. Those answers are worse when the model thinks less, and they are worse when it is only allowed to see the last 60 days. $50/year is the combination of **maximum reasoning effort (`xhigh`)** and **no history horizon**. Same model. More of your data, thought about harder. Reasoning effort is also the visible quality signal. You can feel the difference: a lookup streams almost immediately; a causal question pauses, reads more, calls tools, and comes back with a sourced argument instead of a vibe. ## Concrete examples ### A lookup versus a causal question **Free-depth path.** "What was my average recovery last week?" The coach classifies this as a factual lookup, thinks at low effort, reads a short window, and returns a number. That is the right amount of work. **Member-depth path.** "Why has my recovery been sliding since April, and is it the late training or the travel?" The coach classifies this as causal, raises reasoning to `xhigh`, loads months of history rather than a 60-day slice, retrieves the April stretch, and can call tools for the recovery series and an on-demand relationship analysis. The answer names what survived partial-correlation controls and what did not. See [analytics](/docs/analytics) and [AI coach](/docs/ai-coach#grounding). The $50 difference is not a prettier paragraph about last week. It is being allowed to ask the April question and get an answer that actually used April. ### A belief the data contradicts You told the coach that late training wrecks your sleep. That is a stored [learning](/docs/concepts#learnings). On a schedule, Member-quality coaching cross-examines testable beliefs against the precomputed statistics and marks them confirmed, contradicted, or untested. The sentence that is worth $50 is: "You believe late training wrecks your sleep; across 340 days it does not." A 60-day window can accidentally agree with you. Full-history reasoning is what makes the contradiction visible, and `xhigh` is what lets the coach say it carefully instead of either parroting the belief or overclaiming. See [the belief check](/docs/ai-coach#the-belief-check). ### A pattern from eight months ago "What changed the week I started creatine?" is only a good question if the coach can find that week. - Vector-only retrieval over the last 60 days will miss it if the stretch is older than the horizon. - Member retrieval is **hybrid** — vector search and full-text search fused, then reranked — over the full record. The week you actually wrote "started creatine" is findable even if it was eight months ago. See [embedding and hybrid retrieval](/docs/concepts#embedding-and-hybrid-retrieval). ### Whether the mission is actually moving A [Mission Statement](/docs/mission-statement) is one goal and three commitments. Deterministic progress charts stay free — anyone can see whether the boxes are getting checked. **Mission Analysis** is the Member-only generative pass: the coach thinks at maximum effort about whether those commitments are causally related to the goal, not just correlated with showing up. That is the difference between "you ran 12 times" and "the running is not the lever; sleep consistency is." ### One extra tool round The in-app coach and the [MCP tools](/docs/mcp-tools) share one implementation. Two tool rounds is enough to fetch a streak and stop. Three rounds is enough to fetch the streak, then the correlation it depends on, then synthesise. The extra round is how "your recovery streak is 9" becomes "your recovery streak is 9, and it tracks prior-day bedtime, not training load." ## What stays free The ledger, WHOOP sync, CSV import and export, charts, and the nightly correlation engine stay available without a membership. Deterministic insights are cheap to compute and they are what make the free tier worth using long enough to accumulate the history that makes the upgrade valuable. nLight.fit does not ration the *tracking* product. It rations how much of that track the coach is allowed to reason over at maximum depth. ## Signing up and upgrading A free account is open to anyone at [nlight.fit/app](/app) — no card, no waitlist. Upgrade from [Pricing](/#pricing) or from Settings in the app. Checkout is attached to your signed-in account; a public Stripe link is never used. ## Related - [Overview](/docs/overview) — what the platform is, including how to upgrade from Pricing - [AI coach](/docs/ai-coach) — how a question is classified and grounded - [Concepts](/docs/concepts) — retrieval, learnings, Mission Statement - [Docs assistant](/docs/docs-assistant) — the public overlay that will walk you to these pages --- # Quickstart About ten minutes of setup, then the useful part starts accumulating on its own. ## 1. Create an account Go to [nlight.fit/app](/app) and choose "new? register". A username and a password is the whole form — there is no waitlist and no card. You are signed in immediately, on the free plan. Free covers all the tracking and gives you the coach over your last 60 days. Member is $50/year, tax included — upgrade from Pricing or from Settings once you are signed in. There is no password reset yet, so store your password somewhere you will still have it in six months. ## 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 `AM1` 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 from Settings → MCP / CLI Access (see [token management](/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, date, or list), an optional unit, a set of aliases so `hrv` and `push-ups` resolve correctly, and — if it participates in analysis — a domain, a role and a purpose. ## 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. `_healthyEating` grades the day's logged meals. They have no column in the table but are first-class for analysis. Derived ids are prefixed with an underscore. ## Purpose What a metric is *for*, which is separate from whether it can be analysed. Three values: - **`analytical`** — a real signal, and the default. Almost every metric is this. - **`display_only`** — tracked for your benefit rather than as a modelling input. WHOOP's own weight reading is the current example: it duplicates the weight column you maintain by hand, so it is worth showing and not worth correlating. - **`retired`** — kept so history stays readable, no longer tracked going forward. This exists to answer one question honestly: when a metric has no correlations, is that because nothing was found, or because it was never meant to be modelled? Without the distinction both look identical, and "no relationship detected" is a much stronger claim than "never tested". `list_metrics` returns the purpose for exactly this reason. Purpose is independent of the discovery scan. A metric can be `analytical` and still sit outside the nightly scan — see the [metric reference](/docs/metrics#discovery-catalog) for why the two catalogs differ in size. ## 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 "alcohol" and the column is called "Dry January". ## 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. Every edge in the graph carries exactly one of four tiers: | Tier | What it survived | | --- | --- | | `robust` | Multiple-testing correction, confounder controls, and a split-half stability check, on enough days for that check to mean something | | `probable` | Ordinary significance and a real partial correlation, but not the full robust bar | | `hypothesis` | Consistent with the data, and nothing more. A relationship computed on demand for a pair nobody precomputed is always this, no matter how strong the coefficient looks | | `confounded` | The raw correlation looked real and then collapsed once time trend and sleep were controlled for. The association exists; the direct effect does not | `confounded` is the tier most worth understanding, because it is a finding rather than a failure. It says the two metrics do move together and the reason is something else — usually that both drifted over the same months. ## Prior status Not to be confused with a tier. A **status** is the verdict on a relationship the platform already expected to find, from known physiology: sleep should drive recovery, strain should suppress it. **Confirmed** means it held up in your data. **Contradicted** means it did not, which is often the more useful result, because it is the one no textbook could have told you. A tier grades the statistical confidence of an edge the engine discovered. A status grades a prediction the engine went looking for. Filtering the graph for `confirmed` finds nothing, because `confirmed` was never a tier. ## 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 "alcohol" finds a column named "Dry January", and why a column called "AM1" 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 **3**, **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 | Purpose | In discovery scan | | --- | --- | --- | --- | --- | --- | | `wakeAt7am` | 7am wake | habits | behavior, binary | analytical | yes | | `reading30min` | Reading | habits | behavior, binary | analytical | yes | | `takeVitamins` | Vitamins | habits | behavior, binary | analytical | yes | | `weight` | Weight | fitness | outcome | analytical | yes | | `pushUps` | Push-ups | fitness | outcome | analytical | yes | | `sitUps` | Sit-ups | fitness | outcome | analytical | no — logged too rarely for a daily correlation to mean anything | | `oneMileRun` | Mile time | fitness | outcome | analytical | no — logged too rarely for a daily correlation to mean anything | | `stairsClimbed` | Stairs climbed | fitness | outcome | analytical | no — logged too rarely for a daily correlation to mean anything | | `whoopRecovery` | Recovery | whoop | outcome | analytical | yes | | `whoopHrv` | HRV | whoop | outcome | analytical | yes | | `whoopSleep` | Sleep performance | whoop | outcome | analytical | yes | | `whoopStrain` | Strain | whoop | both | analytical | yes | | `whoopRhr` | Resting HR | whoop | outcome | analytical | yes | | `whoopDeepSleep` | Deep sleep | whoop | outcome | analytical | yes | | `whoopCalories` | Calories burned | whoop | outcome | analytical | no — derived from heart rate, so it restates strain | | `whoopSleepEfficiency` | Sleep efficiency | whoop | outcome | analytical | yes | | `whoopDisturbances` | Sleep disturbances | whoop | outcome | analytical | yes | | `whoopSpo2` | SpO2 | whoop | outcome | analytical | yes | | `whoopRespRate` | Respiratory rate | whoop | outcome | analytical | yes | | `whoopRemSleep` | REM sleep | whoop | outcome | analytical | yes | | `whoopLightSleep` | Light sleep | whoop | outcome | analytical | no — largely total sleep minus deep and REM, which are both present | | `whoopSleepCycles` | Sleep cycles | whoop | outcome | analytical | no — tracks total sleep duration almost exactly | | `whoopSkinTemp` | Skin temperature | whoop | outcome | analytical | yes | | `whoopAvgHr` | Average HR | whoop | outcome | analytical | no — moves with strain, which is present and better understood | | `whoopMaxHr` | Peak HR | whoop | outcome | analytical | no — a workout artifact rather than a daily signal | | `whoopTimeInBed` | Time in bed | whoop | both | analytical | no — sleep hours plus awake time, both present | | `whoopAwakeTime` | Awake time | whoop | outcome | analytical | no — the inverse of sleep efficiency, which is present | | `whoopSleepConsistency` | Sleep consistency | whoop | behavior | analytical | yes | | `whoopSleepNeed` | Sleep need | whoop | outcome | analytical | no — computed by WHOOP from recent debt, strain and naps | | `whoopSleepDebt` | Sleep debt | whoop | both | analytical | no — computed by WHOOP from the sleep history it would be correlated against | | `whoopNapTime` | Nap time | whoop | behavior | analytical | yes | | `whoopNapCount` | Nap count | whoop | behavior | analytical | yes | | `whoopWeight` | Weight (WHOOP) | whoop | outcome | display_only | no — duplicates the manually tracked weight column | | `whoopWorkoutCount` | Workout count | fitness | behavior | analytical | no — training load is represented by strain and training time | | `whoopWorkoutTime` | Training time | fitness | behavior | analytical | yes | | `whoopWorkoutDistance` | Workout distance | fitness | behavior | analytical | no — sport-dependent, so it is not comparable across days | | `whoopZone1` | HR zone 1 minutes | fitness | behavior | analytical | no — strain and calories are computed from HR-zone minutes | | `whoopZone2` | HR zone 2 minutes | fitness | behavior | analytical | no — strain and calories are computed from HR-zone minutes | | `whoopZone3` | HR zone 3 minutes | fitness | behavior | analytical | no — strain and calories are computed from HR-zone minutes | | `whoopZone4` | HR zone 4 minutes | fitness | behavior | analytical | no — strain and calories are computed from HR-zone minutes | | `whoopZone5` | HR zone 5 minutes | fitness | behavior | analytical | no — strain and calories are computed from HR-zone minutes | | `mealQualityScore` | Meal quality (AI) | nutrition | behavior | analytical | yes | | `mealProteinEst` | Protein intake | nutrition | behavior | analytical | yes | | `mealUltraProcessed` | Ultra-processed eating | nutrition | behavior, binary | analytical | yes | | `_exercise` | Exercise | fitness | behavior, binary | analytical | yes | | `_healthyEating` | Eating quality (manual) | nutrition | behavior | analytical | yes | | `_sleepHours` | Sleep hours | whoop | behavior | analytical | yes | ## Purpose What a metric is for, which is a different question from whether it can be analysed. | Purpose | Meaning | | --- | --- | | `analytical` | A real signal, and the default. Correlations against it are meaningful, and an absence of them means none were found. 46 of 47 metrics. | | `display_only` | Tracked for the account holder rather than as a modelling input — a duplicate of a hand-maintained column, or a figure that is useful to see and not useful to correlate. Retrievable, but an absent correlation says nothing. `whoopWeight`. | This is why the distinction is worth carrying: without it, "no correlations for this metric" and "this metric was never a correlation input" look identical, and the first is a far stronger claim than the second. `list_metrics` returns the purpose alongside each metric so a client can tell them apart. ## 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. ### The belief check Something you told the coach and something your data shows are two different kinds of claim, and the second does not automatically win. But when they disagree, that disagreement is the most useful thing in the record. So on a schedule, each stored belief is cross-examined against the precomputed statistics and marked **confirmed**, **contradicted**, or **untested**, with the evidence cited. The result is available to the coach, which is what lets it say "you believe late training wrecks your sleep; across 340 days it does not" instead of quietly agreeing with you. Only two kinds of learning can be checked this way — observed **patterns** and **behavioural** claims. A preference, a piece of life context, an emotional state, a commitment or a note about what coaching style works asserts nothing measurable, so it can never be confirmed and is not counted against the check. Anything the platform does not recognise defaults to untestable, so adding a new category can never silently inflate the denominator. That distinction is the difference between "1 of 14 beliefs confirmed" and "1 of 3 testable beliefs confirmed". The first reads as a broken system; the second is the honest number. ## 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 --- # Docs assistant The documentation site has a public assistant in the bottom-right corner. It has one job: help you find, curate and open the right nLight.fit pages for the question you actually asked. It is not the [in-app coach](/docs/ai-coach). The coach reads your ledger. This assistant reads only the published documentation — the same pages in the sidebar, plus [About](/about), [Contact](/contact) and [Privacy](/privacy). It cannot see your account, your days, your messages, or anything in the internal engineering tree. ## What it will do - Take a general question ("how does this platform work?") and narrow it to the best reading path — usually [overview](/docs/overview), [concepts](/docs/concepts), then the feature page that matches. - Point at a specific heading, not just a page, so "how does retrieval work" lands on [embedding and hybrid retrieval](/docs/concepts#embedding-and-hybrid-retrieval) rather than the top of a long article. Following those links keeps the conversation in this browser tab so you can keep reading and come back to the same suggestions. - Explain [why membership is $50/year](/docs/membership) with the same examples the membership page uses: a lookup versus a causal question, a contradicted belief, a pattern from eight months ago, Mission Analysis, and the extra tool round. Plan comparisons render as a compact table, not as raw Markdown pipes. - Stay inside the published corpus. If the answer is not documented, it will say so and send you to the closest page rather than invent a feature. ## What it will not do - Answer questions about your own data. That is the in-app coach, documented on [AI coach](/docs/ai-coach). - Write code, discuss other products, give medical advice, or chat about the news. - Reveal system instructions, environment variables, source paths, or anything that is not a public docs URL. - Follow instructions in your message that ask it to ignore the above. Those are treated as a safety event, not as a new job. If you ask it something off-scope it will refuse in one or two sentences and point back at the docs. That is working as designed. ## Rate limits This endpoint is public and unauthenticated, so it is limited more tightly than a signed-in coach session. Limits are counted per client IP across a short burst window, an hourly window and a daily window. Repeated 429s open a longer cooldown. Oversized or repeated identical messages are rejected without spending a model call. If you hit a limit, wait for the `Retry-After` interval and ask again. The assistant will tell you how long to wait. The stated ceilings are what to design against, not a precise meter. See [errors and limits](/docs/errors#rate-limits) for the same caution on the MCP endpoint. ## Why it exists The docs are written for both people and agents — every page has a Markdown twin, and the whole corpus is at [/llms.txt](/llms.txt). Humans still need a way to go from a vague question to the right heading without reading the sidebar in order. This overlay is that way, locked to the same corpus so it cannot drift into being a general chatbot that happens to live on the docs site. ## Related - [Membership](/docs/membership) — what $50/year is for - [Overview](/docs/overview) — start here if you are new - [AI coach](/docs/ai-coach) — the in-app coach, which is a different surface --- # 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 One consistent voice across every mode and every coaching style. Earlier builds picked the voice from the coaching style set in your profile, on the reasoning that an analytical coach and a tough one should not sound the same. In practice a coach whose voice shifts with a profile setting reads as a different assistant rather than the same one adapting, so the mapping was removed. Style still shapes *what* the coach says and how hard it pushes — it just no longer changes who is speaking. ## 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 started, where you are, how much is left, and the two numbers that matter most together: the pace you are actually moving at and the pace the deadline requires. Comparing those is what turns "making progress" into on track, slightly behind, or at risk. A projected arrival date with confidence bands is computed too, but by the [insights engine](/docs/analytics#precomputed-models) against your profile's goal weight rather than by the Mission Statement itself. The two answer different questions — one asks whether you will hit your deadline, the other when you would arrive if nothing changed — and it is worth knowing they are independent, because they can disagree. **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 across every module at once — habits, nutrition, wearables, bloodwork, genetics — on the deterministic tier of the background pipeline, every couple of hours. See [when things run](#when-things-run). **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. Every edge is graded `robust`, `probable`, `hypothesis` or `confounded`, and that grade is the first thing to read — a coefficient without its tier is not a finding. The verdict on a relationship the engine *expected* to find is a separate vocabulary from the tier on one it discovered; [confidence tier](/docs/concepts#confidence-tier) and [prior status](/docs/concepts#prior-status) explain which is which and why filtering the graph for `confirmed` returns nothing. **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 score | How prepared you are today, as a single score plus the contributors that moved it | | Recovery formula | Which of your behaviours actually move your recovery, and by how much | | 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 | ### Your recovery formula Worth describing on its own, because it is the model most likely to be quoted back at you and the one with the most ways to be wrong. It is a regression of today's recovery on last night's sleep and yesterday's behaviour, reported as **recovery points per unit** — "sleep duration: +6 recovery points per extra hour" — alongside the R² and the number of days it was fitted on. Prior-day strain and sleep duration are always in the model. Exercise, vitamins and eating quality join it conditionally. Two guards decide what gets stated, and both exist because the model was confidently wrong without them. **A behaviour has to be logged often enough to model.** An optional behaviour earns a slot only if it is actually recorded on at least 30% of the days being modelled. The reason: a habit you log a seventh of the time, with every unlogged day treated as "didn't do it", produces a coefficient that measures *whether you were engaged enough to fill in the row* rather than whether the habit did anything. That is not a subtle bias — it is how "prior-day vitamins: −9.9 recovery points" reached the coach as a finding. Below the floor the term is dropped rather than hedged, because there is nothing there to hedge. **A term the correlation engine calls confounded is labelled as such.** Each surviving term is matched back to its edge in the correlation graph, and a `confounded` edge makes the coefficient read `[confounded — not a direct effect]` wherever the formula is stated. The coefficient is real and stays in the model; what it is not is a lever you can pull. A term the engine has no edge for is left plain rather than guessed at — the label is only ever added, never inferred. R² is in-sample fit. It describes how well the model explains the history it was built from, which is not the same as how well it predicts tomorrow. ## 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 The pipeline is split in two by cost, which is the thing to understand if you are reasoning about how fresh a number is. | Worker | Schedule | Does | | --- | --- | --- | | Derived tier | Every 2 hours | Correlations, the insights models, personal records, period summaries, the all-time fact index, labs, methylation and supplement pipelines | | Generative tier | 01:00, 05:00, 10:00, 14:00, 17:00, 21:00 UTC | The steps that call a reasoning model: meal enrichment, the narrative journey, mission analysis, and the belief check against your learnings | | WHOOP sync | Hourly, per your own clock | Pulls at 05:00–09:00 then every two hours to 23:00, in your timezone | | Embeddings | 06:00 UTC | Batch-embeds anything flagged during the day | | Learning maintenance | 04:00 UTC | Decay, expiry, consolidation | **Why the split.** Correlations and insights are seconds of arithmetic; mission analysis alone is allowed ten minutes of reasoning-model time. Running them in one loop meant the cheap work inherited the expensive cadence — a correlation refreshed only as often as the prose did — and one slow account could exhaust the budget before the rest of the queue was reached. Separating them lets the arithmetic run several times a day. **WHOOP sync is on your clock, not UTC.** The cron fires hourly and each account is pulled when its own local time reaches one of the scheduled hours, so everyone gets the same schedule in their own day wherever they live. It is hourly through the morning because that is when WHOOP scores the night's sleep and the day's recovery, then every two hours to follow strain as it accumulates. The overnight stretch is skipped: nothing new is scored while you are asleep. 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, unencrypted, 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 Twelve scheduled pulls a day, on your clock rather than a UTC one: hourly from 5 AM to 9 AM, then every two hours until 11 PM. The morning is dense because that is when WHOOP scores the night's sleep and the day's recovery. Overnight is left alone — nothing is scored while you are asleep. 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. When a webhook has already refreshed you in the last 45 minutes, the scheduled pull stands down instead of re-fetching the same data. That makes the schedule a safety net rather than the primary path. Webhook deliveries are not guaranteed, so the pulls exist to close any gap left by one that never arrived. 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, plus full consumer panels running to 120+ markers. You review what was extracted before it is saved, which matters because lab PDFs vary and an occasional marker lands in the wrong row. Two things reports disagree about, which the platform handles rather than flattening: **Some reports print ranges, others print a verdict.** A performance-oriented panel gives you both a reference range and an optimal range, so a value can sit inside the first and outside the second. A consumer panel often prints no numbers at all — just "In Range" or "Below Range" beside each result. Those are read differently on purpose: "in range" from an optimal band means *in range but not where you want it*, while "In Range" printed by the lab means the lab called it normal and there is no optimal band to be outside of. Markers a report rates neither way are shown as **not rated** rather than quietly counted as fine. **Not every result is a number.** A positive ANA screen, a blood group, and most of a urinalysis are words. They are kept and displayed with the rest of the panel, but they are excluded from trends and correlations, because there is nothing to plot. ## What you get **A panel overview** — every marker with its value, unit, reference range, and how the report rated it, ordered so anything flagged comes first. **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." Which behaviours get tested against which markers is declared per marker rather than swept blind. Testing every habit against every marker on a 120-marker panel means thousands of comparisons, and at three or four panels a strong-looking correlation arises by chance often enough to be worthless. Lipoprotein(a), for instance, is essentially genetically fixed, so it is deliberately never correlated against behaviour — a relationship found there could only be noise. **Cross-marker reading.** Some findings only exist between markers. A normal LDL cholesterol beside a high LDL particle count is the classic one: each row looks fine alone, and the discordance between them is the actual result. Markers that need to be read together are linked so they surface together. ## 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. **Extracted markers are not field-level encrypted.** The PDF stays on your device; the structured panel is stored in the clear inside the database. See [security and privacy](/docs/security#encryption). ## 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. **A cross-check against what you already take.** The half of this that recommends is useless without the half that checks. So the genetics result is cross-referenced against your [supplement regimen](/docs/mcp-tools#get-supplements) and states three things separately: which of its suggested compounds you are already taking, which are genuinely still open, and which item in your current stack your own variants argue against. Without this, the obvious failure is a confident suggestion to start something that has been in your cupboard for a year. An empty regimen and an unrecorded one are reported as different facts. If nothing is on file you are told that it is unknown, not that you take nothing. ## Unparsed genotypes are UNKNOWN, never normal If a genotype cannot be read out of the uploaded report, that gene is reported as **unknown**. It is not folded in with the genes confirmed to carry no variant. The distinction matters more here than almost anywhere else in the platform. "No variant found" is a reassurance about your genetic status; "we could not read this" is an absence of information. Collapsing the second into the first manufactures the reassurance, and a reader has no way to tell it was manufactured. A gene only counts as normal when it has at least one successfully parsed variant and every parsed variant came back negative. ## Asking about it `get_methylation_data` returns the full pathway overview with watch markers, priors and the regimen cross-check, 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. **Genotypes are not field-level encrypted.** The PDF stays on your device; the structured profile is stored in the clear inside the database. See [security and privacy](/docs/security#encryption). ## 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 --- # nLight.fit developer resources Everything nLight.fit publishes for people and programs building against it, with the URL of each and a request you can run for every one. If you are an agent deciding whether this API is integrable, this page and the documents it links are the whole answer. This page is also served at [`/api`](/api) and [`/api-docs`](/api-docs), and as raw Markdown at [`/docs/developers.md`](/docs/developers.md). ## Start here **The MCP server is the intended integration point.** nLight.fit exposes a member's health record as read-only [Model Context Protocol](https://modelcontextprotocol.io) tools over Streamable HTTP, so an MCP client calls them natively rather than through a REST wrapper you have to write. The REST endpoints exist because the web app uses them. **Nothing below needs an account to try.** The handshake and the tool catalog on the real endpoint need no credential, and the sandbox needs none for anything at all. ### Call the sandbox [`https://nlight.fit/api/sandbox`](/docs/sandbox) speaks the same protocol with the same tool schemas against a synthetic member record. No account, no token, no approval: ```bash curl -sS https://nlight.fit/api/sandbox \ -H 'Content-Type: application/json' \ -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}' ``` Every tool works. Every value is generated, and every response says so. ### Handshake with the real server `initialize` and `tools/list` are answered on the production endpoint without a credential too, so you can confirm the server is real and see the tool surface before anyone signs up: ```bash curl -sS https://nlight.fit/api/mcp \ -H 'Content-Type: application/json' \ -d '{"jsonrpc":"2.0","id":1,"method":"initialize", "params":{"protocolVersion":"2026-07-28","capabilities":{}, "clientInfo":{"name":"my-client","version":"1.0.0"}}}' ``` ```bash curl -sS https://nlight.fit/api/mcp \ -H 'Content-Type: application/json' \ -d '{"jsonrpc":"2.0","id":2,"method":"tools/list"}' ``` `tools/call` is where a credential becomes necessary, because that is where a member's record is read. ### Or use the CLI ```bash npm install -g https://nlight.fit/cli/nlight-fit-latest.tgz nlight discover # fetch every developer document listed below nlight init # MCP handshake against the sandbox nlight tools # list the tools nlight call get_streak metric=vitamins ``` Installing from that URL needs no registry account — `npm` supports tarball URLs natively, and it is the same artifact `npm publish` would upload. Registry publication is pending; `x-cli.registryPublished` in [the OpenAPI document](/openapi.json) says whether `npm install -g nlight-fit` works yet. See [the CLI reference](/docs/cli) for the full command set and how to point it at production. ## Machine-readable resources | Resource | URL | Format | | --- | --- | --- | | nLight.fit OpenAPI specification | `https://nlight.fit/openapi.json` | `application/openapi+json` | | — alternate location | `https://nlight.fit/.well-known/openapi.json` | `application/openapi+json` | | nLight.fit API catalog (RFC 9727) | `https://nlight.fit/.well-known/api-catalog` | `application/linkset+json` | | nLight.fit MCP server manifest | `https://nlight.fit/.well-known/mcp.json` | `application/json` | | — short alias | `https://nlight.fit/mcp.json` | `application/json` | | OAuth protected resource metadata (RFC 9728) | `https://nlight.fit/.well-known/oauth-protected-resource` | `application/json` | | OAuth authorization server metadata (RFC 8414) | `https://nlight.fit/.well-known/oauth-authorization-server` | `application/json` | | Documentation index for language models | `https://nlight.fit/llms.txt` | `text/plain` | | Full documentation corpus | `https://nlight.fit/llms-full.txt` | `text/plain` | Every documentation page is also served as raw Markdown at `/docs/.md` — for example [`/docs/mcp.md`](/docs/mcp.md) — with no navigation chrome to strip. Every HTML page on the site serves Markdown under `Accept: text/markdown`. ## Endpoints | Endpoint | What it is | Credential | | --- | --- | --- | | `POST /api/mcp` | The nLight.fit MCP server. Eighteen read-only tools over a member's record. | Bearer token with `read`, for `tools/call`. The handshake and `tools/list` need none. | | `POST /api/sandbox` | The same protocol and tools against synthetic data. | None, ever. | | `GET /openapi.json` | This API as OpenAPI 3.1. Fifteen operations, all with typed schemas. | None. | | `GET /.well-known/api-catalog` | RFC 9727 linkset naming every published API. | None. | | `GET /.well-known/mcp.json` | The MCP manifest: transport, protocol revisions, tool list, access terms. | None. | | `POST /api/proxy` | The web app's data endpoint, dispatching on an `action` field. | Bearer token, except `healthCheck`. | | `POST /api/tokens` | Mint, list and revoke your own API tokens. | Signed-in session. | A version alias exists at `/api/v1/*` for tooling that expects a version segment in the path. It routes to exactly the same handlers; see [versioning and deprecation](/docs/versioning). The full operation list, with request and response schemas for each, is in [the OpenAPI document](/openapi.json). It is built from the same constants the handlers use, so a documented operation that no longer exists fails the build rather than misleading a generated client. ### Service status `healthCheck` is the one REST action that needs no credential, and it is what the API catalog names as this API's status link: ```bash curl -sS https://nlight.fit/api/proxy \ -H 'Content-Type: application/json' \ -d '{"action":"healthCheck"}' ``` ```json { "version": "3.1.0", "status": "healthy", "timestamp": "2026-08-23T00:00:00.000Z", "mongodb": "connected" } ``` A signed-in caller gets the same fields plus operational detail. Anyone can poll the four above. ## Authentication There are two routes, and which one you want depends on whose record you are reading. ### Your own account: three calls, no human Signup is open, so a program can go from nothing to calling tools without anyone approving anything: ```bash # 1. Create an account. The response carries a session token. curl -sS https://nlight.fit/api/auth \ -H 'Content-Type: application/json' \ -d '{"action":"register","username":"my-agent","password":"..."}' # 2. Exchange that session for a read-scoped API token. curl -sS https://nlight.fit/api/tokens \ -H 'Content-Type: application/json' \ -H 'Authorization: Bearer ' \ -d '{"action":"create","name":"my-agent","scopes":["read"]}' # 3. Use it. curl -sS https://nlight.fit/api/mcp \ -H 'Content-Type: application/json' \ -H 'Authorization: Bearer rt_ro_...' \ -d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"list_columns","arguments":{}}}' ``` This is the path `credentialSteps` in `x-access-terms` describes, with `humanInvolved: false` on every step. A member can do step 2 from Settings → MCP / CLI Access instead. ### Somebody else's account: OAuth 2.1 A client reading a *member's* record — Claude connecting to someone's health history — goes through OAuth, because that is where a person decides to share it. 1. An unauthenticated `tools/call` returns `401` with `WWW-Authenticate: Bearer realm="routine-tracker-mcp", resource_metadata="https://nlight.fit/.well-known/oauth-protected-resource"`. 2. That document names the authorization server. Its metadata is at [`/.well-known/oauth-authorization-server`](/.well-known/oauth-authorization-server). 3. Register dynamically ([RFC 7591](https://www.rfc-editor.org/rfc/rfc7591)) at `POST /api/oauth/register`. Public clients only — there is no client secret. 4. Send the member through `GET /api/oauth/authorize` with a PKCE `code_challenge` (`S256`; `plain` is not accepted). They approve a consent screen. 5. Exchange the code at `POST /api/oauth/token` with the `code_verifier`. **Step 3 is open, and you can do it right now.** Registration needs no credential and no human: ```bash curl -sS https://nlight.fit/api/oauth/register \ -H 'Content-Type: application/json' \ -d '{"client_name":"my-client", "redirect_uris":["https://example.com/callback"], "grant_types":["authorization_code","refresh_token"], "response_types":["code"], "token_endpoint_auth_method":"none"}' ``` ```json { "client_id": "…", "client_id_issued_at": 1787450670, "token_endpoint_auth_method": "none", "scope": "read" } ``` Step 4 is the one step in *this* route that a program cannot complete, and that is deliberate: it is where a person decides to share their own medical record. It is published as data under `delegatedAccess` in `x-access-terms`, alongside the fully automatable `credentialSteps` above, so a client can pick the route that matches what it is doing rather than discover the boundary by hitting it. In Claude, all of that is *Add custom connector* and pasting `https://nlight.fit/api/mcp`. Tokens are `read`-only today, carry their scopes in the database rather than in the token string, and are revoked individually and immediately. Full detail in [authentication](/docs/authentication) and [token management](/docs/token-scripts). ## Conventions every response follows **Rate limits are reported, not just enforced.** Every response carries `RateLimit-Policy` describing the quota, and every response that spends or reads one carries `RateLimit` describing what is left, so a client paces itself instead of discovering the limit by hitting it. A `429` adds `Retry-After`. ```http RateLimit-Policy: "mcp";q=120;w=60 RateLimit: "mcp";r=118;t=47 RateLimit-Limit: 120 RateLimit-Remaining: 118 RateLimit-Reset: 47 ``` `RateLimit` and `RateLimit-Policy` follow the IETF structured-field draft; the three-header form is the earlier draft, sent alongside because that is what most existing SDKs read. The complete quota table is published as data under `x-ratelimit-policies` in the OpenAPI document. Details in [errors and limits](/docs/errors). **The version is on every response.** `Api-Version` names the API version that answered. Deprecation, when it happens, arrives as a `Deprecation` header ([RFC 9745](https://www.rfc-editor.org/rfc/rfc9745)) and then a `Sunset` header ([RFC 8594](https://www.rfc-editor.org/rfc/rfc8594)), with notice periods stated in the [versioning policy](/docs/versioning). **Every response links back to this documentation.** The `Link` header carries `service-desc` (the OpenAPI document), `service-doc` (this page) and `version-history` (the versioning policy), so a client holding any response can find the rest without a discovery request. **Errors are structured.** REST failures return `error`, a stable `code` where one applies, and a `requestId` to quote in a support request. MCP failures are JSON-RPC error objects with the standard codes. Both tables are in [errors and limits](/docs/errors). ## Human-readable documentation - [MCP server](/docs/mcp) — transport, methods, which need no credential, scope, and what is deliberately not exposed. Also at [`/mcp`](/mcp). - [MCP tool reference](/docs/mcp-tools) — all eighteen tools with full input schemas. - [Client setup](/docs/clients) — connecting Claude Desktop, Claude Code or Cursor. - [Sandbox](/docs/sandbox) — the credential-free environment and what its data is and is not. Also at [`/sandbox`](/sandbox). - [Authentication](/docs/authentication) — token format, scopes, storage and revocation. - [nLight.fit CLI](/docs/cli) — `npm install -g https://nlight.fit/cli/nlight-fit-latest.tgz`. Also at [`/cli`](/cli). - [Token management](/docs/token-scripts) — minting and revoking tokens from Settings. - [Errors and limits](/docs/errors) — status codes, JSON-RPC error codes, rate limits and protocol versions. - [Versioning and deprecation](/docs/versioning) — how change is signalled before it lands. Also at [`/deprecation`](/deprecation). - [Security and privacy](/docs/security) — encryption, third-party handling, and an honest account of what is not protected. ## What it costs Tracking is free, and API and MCP access are part of the free tier. [Member](/docs/membership) is $50/year and changes how hard the coach thinks over a member's own history — it does not change what this API exposes. The machine-readable version is `x-access-terms` in [the OpenAPI document](/openapi.json) and `_meta."fit.nlight".pricing` in [the MCP manifest](/.well-known/mcp.json), with `credentialSteps` giving the path above and `delegatedAccess` giving the OAuth one. **Signup is open, so nothing here needs a human.** Building against [the sandbox](/docs/sandbox) first is still the better order — you never create an account you do not need, never handle someone's real bloodwork while you are still getting the shapes right, and you develop against a populated record rather than an empty one. Then change one URL: the tool names, schemas, validation and response envelopes are the same objects in both, not two implementations kept in agreement by hand. ## Contact `Austin@nLight.fit` — one mailbox, for developer questions and security reports alike. Please report security issues privately and allow a reasonable window for a fix before disclosure. nLight.fit is based in Boston, MA. --- # nLight.fit 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 Eighteen tools, in three groups. **Curated tools** are the eleven 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. **Account tools** are `get_profile`, which returns what you have said about yourself rather than anything measured: your Mission Statement and the injuries, allergies and exclusions any recommendation has to respect. Every other tool reports what happened to you; this is the one that reports what you are trying to do and what would harm you, so a client that plans training or nutrition should read it first. Your name and your free-form notes are never exposed — see [what is not exposed](#what-is-not-exposed). The [tool reference](/docs/mcp-tools) lists all eighteen with full input schemas. ## What is not exposed Connecting a client does not hand over your whole profile. Two fields are withheld from MCP on purpose. Your **name** stays out because nothing an external model does with a health record needs it, and it is the single field that turns a de-identified record into an identified one. Clients are told this so they neither ask for it nor guess at it. Your **free-form notes** — the open "anything else" field — stay out because their contents cannot be known in advance, which means they cannot be cleared for sharing in advance either. Coaching-style settings are also withheld, for a different reason: they configure how the in-app coach talks to you, and an external client is there to analyse your record, not to imitate a persona. Everything else in the profile is shared when a client calls `get_profile`, including injuries and allergies. That is deliberate. This endpoint already returns your bloodwork, your genotypes and every metric you track, so withholding the two fields whose whole purpose is preventing a harmful suggestion would protect nothing and cost you safety. ## 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. ## Try it without an account A sandbox at `POST https://nlight.fit/api/sandbox` speaks this exact protocol with these exact tool schemas, answered from a synthetic member record, and needs no credential at all. Point a client at it to learn the surface before anyone signs up, then change the URL. See [sandbox](/docs/sandbox). ## Transport Streamable HTTP. 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. Three consequences worth knowing: - `GET` returns `405`, with `Allow: POST, OPTIONS`. That is what the transport prescribes for a server offering no server-to-client event stream, and this one is stateless with nothing to push. - JSON-RPC batching is rejected. Send one message per request. - Send `Accept: application/json, text/event-stream`, as the transport requires. You will get JSON. A client that accepts only `text/event-stream` gets the same envelope as a single SSE `message` event instead. Every response carries `MCP-Protocol-Version` naming the revision that answered, `RateLimit-Policy` describing your quota, and `Api-Version`. A response that spends quota also carries `RateLimit` with what is left of it. Send a credential and every request is counted. Send none and an anonymous `tools/list` is still counted, while the negotiation methods (`initialize`, `server/discover`, `ping`) are answered without touching the limiter and carry the policy alone. A machine-readable description of the server — endpoint, transport, authorization, protocol revisions and tool list — is published at [`/.well-known/mcp.json`](https://nlight.fit/.well-known/mcp.json). ## Supported methods | Method | Purpose | | --- | --- | | `server/discover` | Stateless capability discovery. Returns protocol version, capabilities, server info and the orientation instructions. | | `initialize` | The legacy handshake. Still answered for older clients; negotiates the highest revision you both speak. Also carries the instructions. | | `tools/list` | The tool catalog, built for the authenticated account. Carries a one-hour cache hint scoped `private`. | | `tools/call` | Invoke a tool. | | `prompts/list` | The published workflows. | | `prompts/get` | Expand one workflow into a message, with arguments filled in. | | `ping` | Health check. Returns an empty object. | There are no MCP resources today. ## Reading a tool result Every tool returns the same envelope. The payload is a JSON document, pretty-printed into a single text block: ```json { "content": [{ "type": "text", "text": "{\n \"found\": true,\n …\n}" }], "isError": false } ``` Two things about that shape are worth designing around. **A tool that fails still returns `200`.** A failure is reported as `isError: true` with the reason inside the payload, not as a JSON-RPC error. That is deliberate: a model can read what went wrong and adjust, whereas a transport-level error tells the client the whole call is broken. Only protocol problems — malformed JSON, an unknown method, bad arguments — come back as JSON-RPC errors. See [errors and limits](/docs/errors). **The text block is capped.** Past the cap the JSON is cut and an explicit `[TRUNCATED: …]` marker is appended, so a clipped series can never read as a complete one. The cap and the exact marker are on the [errors page](/docs/errors#result-size). ## How current is this result? Different tools answer as of different moments, and the difference is not cosmetic. `get_recovery_context` reads the live ledger and answers as of today. `get_user_facts` and `get_insights` reflect the last time the background analysis pipeline ran. Quoting the two side by side as though they described the same moment is the specific error this next field exists to prevent. So a tool result carries an as-of date: ```json { "found": true, "coverage": { "lastDay": "2026-08-09" }, "computedAt": "2026-08-15T12:20:17.721Z", "dataThrough": "2026-08-09" } ``` `dataThrough` is always a plain `YYYY-MM-DD` calendar date. It means **this result describes your record up to and including that day** — not when the result was computed. Where a tool has both, the data horizon wins: the example above was computed on the 15th from a record that ends on the 9th, and `dataThrough` reports the 9th, because that is the date the numbers are actually about. Three rules for using it: - **Do not compare two results as contemporaneous unless their `dataThrough` values agree.** If they differ, say so in your answer rather than resolving the gap silently. - **Absence is not a freshness claim.** A tool with no dated horizon — `list_metrics` returns a catalog, not a series — carries no `dataThrough`, and neither does one whose payload does not expose a date the server can read. Treat a missing field as "unknown", never as "current". - **It is a horizon, not a guarantee of completeness.** A day inside the range can still be unlogged. Coverage fields on the individual tools tell you which days actually hold data. ## Orientation An external client starts colder than the in-app coach. The coach reaches these same handlers through a system prompt built from your profile, your columns, your labs and your correlations. A client connecting over MCP gets the protocol and nothing else, and a model with the right tools and no framing produces confident wrong answers rather than merely inefficient ones. Three things close that gap. **Instructions.** `initialize` and `server/discover` both return an `instructions` string, which clients fold into the model's system prompt. It describes the four layers of the data — ledger, feature matrix, precomputed statistics, narrative — and states the things that are not guessable from a schema: that `r` means a confounder-controlled partial correlation in `get_correlation_edges` and a raw Pearson in the `leadLag` insights section, which confidence tiers exist and what each one survived, how stale each layer can be, and the window and size budgets. It is identical for every account, so it costs nothing to cache. **A catalog built for you.** `tools/list` stamps every tool description with your account's real context: how many days you have logged and over what span, the actual names of your custom columns, whether WHOOP is connected, and whether you have enough history for the correlation and insights engines to have run at all. Without this a model learns your thresholds by calling tools until one refuses. This is why the catalog is marked `cacheScope: private` — it now definitely carries one account's vocabulary, so a shared cache serving it to another user would be a real leak rather than a theoretical one. **Prompts.** Four published workflows — a morning readiness check, a weekly review, a hypothesis test over any two metrics, and a single-metric explainer. Clients that render prompts, Claude Desktop among them, show these as pickable entries, so you can run a multi-tool analysis without knowing the tool graph exists. The hypothesis test is the one worth knowing about: it directs the model to check the precomputed verdict *before* running its own analysis, because an ad-hoc query over a short window will happily rediscover a confound the nightly scan already rejected and present it as a finding. ## 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 endpoint returns a complete medical record, so a request that looks like it came from a browser is refused. Genuine MCP clients are servers and send no `Origin` at all, so refusing every request that carries one costs nothing and closes the DNS-rebinding case the transport specification warns about. ## Connecting over OAuth The server is also an OAuth 2.1 authorization server, which is what makes Claude's *Add custom connector* work: paste the endpoint URL, approve a consent screen, done. No token, no config file. A client with no credential POSTs to the endpoint and gets a `401` whose `WWW-Authenticate` header carries a `resource_metadata` pointer. From there the flow is entirely standard: | Endpoint | Purpose | | --- | --- | | `/.well-known/oauth-protected-resource` | Names the resource and its authorization server ([RFC 9728](https://www.rfc-editor.org/rfc/rfc9728)) | | `/.well-known/oauth-authorization-server` | Endpoint locations and supported parameters ([RFC 8414](https://www.rfc-editor.org/rfc/rfc8414)) | | `POST /api/oauth/register` | Dynamic client registration ([RFC 7591](https://www.rfc-editor.org/rfc/rfc7591)) | | `GET /api/oauth/authorize` | Consent screen | | `POST /api/oauth/token` | Code exchange and refresh | Four constraints are worth stating because they are enforced rather than recommended: - **Public clients only.** No client secret is issued. A desktop application cannot keep one, so issuing them would create a credential that looks confidential and is not. PKCE carries the proof instead. - **`S256` only.** The `plain` challenge method is advertised nowhere and accepted nowhere. - **`read` only.** The authorization server cannot issue a write scope at all. A connector asking for `write` is granted `read`, so no consent screen can be the thing that lets an outside application modify a medical record. - **Authorization codes are single-use, and a replay is treated as theft.** Presenting a code twice means one of the two holders stole it and the server cannot tell which, so it revokes the entire grant — an attacker who wins the race still ends up with nothing. The access token a connector receives is an ordinary API token. It appears in your token list under the name of the application that asked, it carries the same `read` scope, and revoking it disconnects the client immediately. ## Getting connected If your client speaks OAuth, paste `https://nlight.fit/api/mcp` into its connector dialog and approve the screen. Otherwise: 1. Mint an API token in Settings → MCP / CLI Access — 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 eighteen back. ## Related - [MCP tool reference](/docs/mcp-tools) — all eighteen tools with input schemas - [Authentication](/docs/authentication) — how API tokens and scopes work - [Errors and limits](/docs/errors) — status codes, rate limits, protocol versions --- # nLight.fit sandbox ``` POST https://nlight.fit/api/sandbox ``` No account. No token. The same MCP protocol and the same eighteen tool schemas as the production server, answered from a member record that was generated rather than lived. ## Why it exists Signup is open, so an agent evaluating this integration *could* register. It should not have to, and mostly it should not want to. An empty account is a bad place to develop against: nearly every interesting tool here answers from months of history, so a brand new record makes the whole surface look like it returns nothing. The alternative — pointing a half-built integration at somebody's real bloodwork — is worse. The sandbox gives you a populated record with no signup, no credential to manage, and no real person's data in the loop while you are still getting the request shapes right. Everything a client needs in order to decide — the tool catalog, the argument schemas, the response shapes, the error behaviour, the rate-limit conventions — is reachable immediately, at the cost of the data being invented. ## Try it ```bash curl -sS https://nlight.fit/api/sandbox \ -H 'Content-Type: application/json' \ -H 'Accept: application/json, text/event-stream' \ -H 'MCP-Protocol-Version: 2026-07-28' \ -H 'Mcp-Method: tools/list' \ -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}' ``` Calling a tool: ```bash curl -sS https://nlight.fit/api/sandbox \ -H 'Content-Type: application/json' \ -H 'Accept: application/json, text/event-stream' \ -d '{"jsonrpc":"2.0","id":2,"method":"tools/call", "params":{"name":"get_recovery_context","arguments":{"days_back":14}}}' ``` To connect an MCP client, use `https://nlight.fit/api/sandbox` as the server URL and leave authentication empty. [Client setup](/docs/clients) covers the per-client configuration; the only difference is that no OAuth step happens. ## What is the same as production - **The tool catalog.** `tools/list` returns the same eighteen tools with the same names, descriptions and input schemas. They are read from the production catalog, not copied, so the two cannot drift apart. - **Argument validation.** Arguments are checked against the same schemas, so a call the production server would reject is rejected here with the same message. - **The transport.** Streamable HTTP, stateless JSON-RPC 2.0 over POST, the same `MCP-Protocol-Version` negotiation, the same `405` on `GET`. - **The rate-limit policy.** The same quota, reported in the same `RateLimit` and `RateLimit-Policy` headers, keyed by source address instead of by member. - **The response envelope.** `content[].text` carrying JSON, `isError` set the same way. ## What is different - **The data is synthetic.** Roughly 180 days of a generated ledger across ten metrics and four habits, plus a fixed lab panel, methylation profile, supplement regimen and Mission Statement. It is deterministic — the same request returns the same numbers — and anchored to today, so the most recent day is always yesterday or today. - **Every response says so.** Each tool result carries `"sandbox": true` and a `notice` field stating in words that the values are generated and belong to no real person. The server instructions delivered at `initialize` say the same thing, so a model connected to the sandbox is told before it reads a single number. - **The analysis is simpler.** Correlations are raw pairwise Pearson over the generated series. The production engine applies partial correlation with covariate controls and Benjamini-Hochberg correction before it will call anything a finding, and assigns a tier accordingly. The sandbox computes its correlations honestly over its own data but claims no tier, and says so in the payload. - **There is no account.** `get_profile` returns a fixed synthetic Mission Statement and constraint list. Nothing is writable and nothing persists. The endpoint never opens a database handle and never resolves a credential, so there is no call by which a sandbox request could reach a real member record. ## The synthetic member Ten continuous metrics — weight, sleep, HRV, resting heart rate, WHOOP recovery and strain, steps, protein, water and mood — and four binary habits: workout, reading, vitamins and no alcohol. The series are shaped rather than random. There is a weekend dip in the behaviour metrics, a slow downward weight trend, and a recovery score derived from that night's sleep, so the correlation the tools report between sleep and recovery is genuinely present in the data rather than asserted over noise. A client that cross-checks a reported correlation against the raw series it also fetched will find the two agree. ## When to stop using it Switch the URL to `https://nlight.fit/api/mcp` and add a Bearer token once a member has connected their account. Nothing else in your client changes — that is the property the sandbox exists to give you. See [MCP server](/docs/mcp) for the production endpoint and [authentication](/docs/authentication) for how tokens are obtained. --- # nLight.fit MCP tool reference The server exposes **18 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_supplements`](#get-supplements) | Curated | Get the user's supplement regimen: what they take, at what dose, on what schedule, and SINCE WHEN. | | [`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. | | [`get_profile`](#get-profile) | Account | The account holder's stated goals and hard constraints: the Mission Statement (the outcome they want, their core goal, its dates, and the commitments they measure themselves against), injuries and allergies, anything they have asked never to be suggested, and what they have already reported worked or failed for them. | ## 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 the user added themselves. ALWAYS use this for custom/non-standard habits. Supports fuzzy matching: 'alcohol' finds a column labelled 'Dry January', 'sauna' finds 'Heat', 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. 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. The inline series is capped at the 50 most recent readings - `data_points` is the true total, `returned` is how many came back, and `stats` always covers the full total, so a cited min/max may predate the visible window. | 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', 'last night'. Last night is the most recent overnight sleep, resolved in the user's timezone and stamped on the WHOOP wake date. | ```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 (for example, a column labelled 'Dry January' with description 'no alcohol'). | 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 | | `date` | `string` | No | Optional YYYY-MM-DD, 'today', 'yesterday', or 'last night'. When set, look up that night (plus the adjacent WHOOP wake-date) instead of treating the most recent day with data as today. | ```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": {} } } ``` ### get_supplements Get the user's supplement regimen: what they take, at what dose, on what schedule, and SINCE WHEN. Use when the user asks about supplements, vitamins, their stack, whether something is actually working, what to add or drop, or when a supplement might explain a bloodwork or recovery change. Each entry carries start/stop dates, so this is also how you find out what changed around a given date. Cross-references their genetics (conflicts and gaps), their bloodwork (marker values before vs during), and - most usefully - `dailyTrials`, which compare daily WHOOP metrics in the weeks before a supplement started against the weeks after, scored against a permutation null so ordinary drift doesn't read as an effect. IMPORTANT: the regimen is self-reported intent with no daily adherence log; trials are observational, can be marked confounded, and must never be stated as proof. | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `supplement` | `string` | No | Optional supplement to look up by name, e.g. 'vitamin d', 'magnesium', 'creatine'. Fuzzy-matched. Omit for the full regimen. | | `include_history` | `boolean` | No | Include stopped supplements and past doses. Default: false for the full regimen, true when a specific supplement is named. | | `include_lab_effects` | `boolean` | No | Include marker values before vs during each supplement, for the biomarkers that compound plausibly moves. Default: true. Requires bloodwork on file. | ```json { "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "get_supplements", "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. Optionally set include_labs to attach a SEPARATE last-observation-carried-forward bloodwork block aligned to the same date axis (for eyeballing lab x daily alignment - NOT for correlating: independent N is the number of draws, not days, and each marker is flagged when draws are too few), and include_genetics to attach the static genotype panel as a stratify-only moderator. 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. A call may return at most 3000 values in total (days x metrics, doubled when z-scores are included), so asking for everything over a long window is rejected with the numbers to fix it. | | `include_z` | `boolean` | No | Include z-scores alongside raw values. Default: true. | | `include_labs` | `boolean` | No | Attach a separate `labs` block: each bloodwork marker last-observation-carried-forward onto the date axis (no backward fill), with asOf/ageDays/nDraws. Labs are step functions sampled a few times a year - each marker is hard-flagged when nDraws<4 (descriptive only) or nDraws=1 (zero-variance). Never enters values/z, never correlate against days. Default: false. | | `include_genetics` | `boolean` | No | Attach a `genetics` envelope: the static genotype panel (affected/normal per gene). A zero-variance moderator to STRATIFY on, never an axis to correlate. Default: false. | ```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` | `robust` \| `probable` \| `hypothesis` \| `confounded` | No | Keep only edges of this confidence tier. 'robust' survived multiple-testing correction, confounder controls and a split-half stability check; 'probable' is nominally significant after controls; 'hypothesis' is everything weaker; 'confounded' means the raw relationship collapsed once time and sleep were controlled for, so it must not be reported as a direct effect. | | `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": {} } } ``` ## Account tools These return what the account holder has said about themselves rather than anything measured: the Mission Statement they are working toward, and the injuries, allergies and exclusions any recommendation has to respect. Read before advising. Name and free-form notes are deliberately never exposed. ### get_profile The account holder's stated goals and hard constraints: the Mission Statement (the outcome they want, their core goal, its dates, and the commitments they measure themselves against), injuries and allergies, anything they have asked never to be suggested, and what they have already reported worked or failed for them. Call this BEFORE offering any recommendation. Every other tool on this server reports what happened to this person; this is the only one that reports what they are trying to do and what would harm them - so a training or nutrition suggestion made without it can contradict an injury or an allergy on file. Self-reported free text, so treat it as intent and constraint, never as an observation or as evidence of progress. Takes no arguments. ```json { "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "get_profile", "arguments": {} } } ``` --- # nLight.fit authentication Machine callers authenticate with a Bearer token: ```http Authorization: Bearer rt_ro_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx ``` Tokens are minted one per client and revoked individually. You can mint one yourself, or let a client obtain one by signing you through the [OAuth flow](/docs/mcp#connecting-over-oauth) — either way the credential that ends up on the wire is the same kind of token, described below. ## 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 `tools/call`, `prompts/list` and `prompts/get`. | | `write` | Anything that changes stored data. Defined and enforced, but nothing over MCP requires it yet. | ## What needs no token at all `initialize`, `server/discover`, `ping`, `notifications/*` and `tools/list` are answered without a credential, so a client can complete the protocol handshake and enumerate the tool surface before it has one. None of them reads a member record. An anonymous `tools/list` returns the generic tool descriptors. The same call with a token returns the same tools described against that member's own column names and date coverage, which is why the personalised catalog is behind the credential and the generic one is not. The result of an anonymous call says so in its `_meta`, rather than leaving you to infer it from descriptions that happen to name no columns. Everything else answers `401` with a `WWW-Authenticate` challenge naming the authorization server — that challenge is what lets an MCP client start the OAuth flow on its own. 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. ## Tokens issued to a connector A client that connects over OAuth receives an ordinary API token. There is no second credential format and no parallel validation path — the authorization server mints through the same code any other token comes from, which is why `/api/mcp` needed no changes to accept one. Three things differ, and all three are recorded on the row rather than encoded in the token: - It carries the name of the application that asked, so you can tell a connector apart from a token you minted by hand. - It expires after 30 days and the client refreshes it silently. Refreshing rotates both credentials and revokes the token it replaced, so a client that refreshes often does not accumulate live credentials. - It is always `read`. The authorization server cannot issue `write` at all, so approving a consent screen can never be the thing that grants an outside application the ability to change your record. Revoking one is the same operation as revoking any other token, and disconnects the client on its next request. ## Why not session tokens A browser session belongs to a person sitting in the app. It can be killed from Settings — change the password, or use **Sign out other devices** — but it is still the wrong shape for a credential that lives inside a third party's cloud and unlocks a complete medical history. So `/api/mcp` accepts only an API token. A session presented there is rejected. Identifying a user any other way is rejected too. ## Related - [Token management](/docs/token-scripts) — minting, listing and revoking tokens from Settings - [Client setup](/docs/clients) — wiring a token into Claude or Cursor - [Errors and limits](/docs/errors) — the full status code table --- # nLight.fit client setup There are two ways in. Claude connects over OAuth: you paste one URL, sign in, and approve — no token, no config file. Everything else takes a bearer token against `https://nlight.fit/api/mcp` over HTTP. If you are using Claude, start below. For the token path, mint one first from Settings → MCP / CLI Access — see [token management](/docs/token-scripts). ## Claude — Desktop, web, and mobile Open **Settings → Connectors → Add custom connector**, paste: ``` https://nlight.fit/api/mcp ``` Claude sends you to nlight.fit, you sign in if you aren't already, and a screen tells you exactly what it will be able to read. Approve it and you land back in Claude with the tools loaded. The connection is **read-only** and there is no way to change that from this flow — the server will not issue a write scope to a connector at all, so nothing you approve here can add, edit, or delete an entry. To end it, revoke the connector's token under Settings → MCP / CLI Access; it appears under the name of the application that asked. This is the same flow on Desktop, on claude.ai in a browser, and in the mobile apps. There is no config file to edit and nothing to keep in sync across devices. ## 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 with a token Only needed if you want Desktop pinned to a specific token rather than an OAuth connection — for a shared machine, or to hold a credential you rotate yourself. Otherwise use the connector above. 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. ## 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 eighteen. 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 a new one from Settings → MCP / CLI Access. | | `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 stops at "you started connecting but didn't finish" | The approval never completed. Reopen the connector and approve it; if the screen never appeared, check that you pasted `https://nlight.fit/api/mcp` with no trailing path. | | The approval screen says the connection can't be completed | The application asked to be sent somewhere it never registered, so the server refused rather than redirect. Nothing was shared. Remove the connector in Claude and add it again. | | Claude reconnects but the tools are gone | The connector's token was revoked in nlight. Remove the connector and add it again to approve a new one. | | 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 — or drop the config entry entirely and use the connector. | ## Good first questions Your client may already offer these. The server publishes four workflows over `prompts/list` — a morning readiness check, a weekly review, a hypothesis test over any two metrics, and a single-metric explainer — and clients that render prompts show them as pickable entries, so you can run a multi-tool analysis without typing one. See [the MCP server page](/docs/mcp#orientation) for what each does. Typed from scratch, 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." - "Read my profile, then suggest a training week that fits my mission and works around my injuries." The fourth is what the warehouse tools exist for: the client does the analysis itself rather than reading a conclusion. The fifth is what `get_profile` exists for — advice that respects what you are actually working toward, and what you have told the platform to avoid. ## Related - [MCP server](/docs/mcp) — transport, methods and access rules - [MCP tool reference](/docs/mcp-tools) — all eighteen tools - [Errors and limits](/docs/errors) — full status code table --- # nLight.fit CLI The official command-line tool for nLight.fit. It speaks the same MCP protocol the production server and the sandbox speak, so an agent or a script can try the integration without building a client from scratch. ## Install The package is published from this site while registry publication is pending: ```bash npm install -g https://nlight.fit/cli/nlight-fit-latest.tgz nlight --help ``` That needs no registry account. `npm` installs from a tarball URL natively, and the tarball is the same artifact `npm publish` would upload. A versioned copy sits beside it for pinning, and [`/cli/index.json`](/cli/index.json) names both along with the current version. Once `nlight-fit` is on npm, the shorter form will work and is preferred — it gives version resolution and updates, which a tarball URL does not: ```bash npm install -g nlight-fit # after registry publication npx nlight-fit tools # after registry publication ``` `x-cli.registryPublished` in [the OpenAPI document](/openapi.json) says which of those is currently true, so a program does not have to guess. ## What it talks to **Sandbox by default.** `nlight tools`, `nlight init` and `nlight call` hit `POST https://nlight.fit/api/sandbox`. No account, no token, no approval. Every value is synthetic and labelled as such. **Production with `--prod`.** `nlight init --prod` and `nlight tools --prod` need no credential either — the server answers the handshake and the generic tool catalog without one, so you can confirm the real endpoint is there and see its tool surface before anyone has an account. **A token, for reading an actual record.** `nlight call --prod` needs `NLIGHT_TOKEN`, a Bearer token with the `read` scope: ```bash NLIGHT_TOKEN=rt_ro_... nlight call get_streak metric=vitamins --prod ``` Setting `NLIGHT_TOKEN` also improves `tools --prod`: the authenticated catalog describes each tool against your own column names and date coverage rather than generically. The token is never sent to the sandbox, which has nothing to authenticate to. Mint that token in the app under Settings → MCP / CLI Access; see [token management](/docs/token-scripts). The published CLI never asks for database credentials and keeps no state. ## Commands | Command | What it does | | --- | --- | | `nlight discover` | HEAD every published developer URL — the OpenAPI document, the MCP manifest, the API catalog, the OAuth metadata, the short aliases — and print the status and content type of each | | `nlight init` | Send MCP `initialize` and print `serverInfo`, `capabilities` and `protocolVersion` | | `nlight tools` | Call `tools/list` and print every tool name | | `nlight call [k=v...]` | Call one tool. Example: `nlight call get_streak metric=vitamins` | | `nlight spec` | Fetch and print `https://nlight.fit/openapi.json` | ```bash nlight discover nlight init nlight tools nlight call get_recovery_context days_back=7 nlight spec | head ``` ## Rate limits and versioning The CLI reads the same `RateLimit`, `RateLimit-Policy` and `Api-Version` headers every other client does. A 429 includes `Retry-After`. Breaking changes are announced with `Deprecation` and `Sunset`; the policy is at [nLight.fit API versioning and deprecation](/docs/versioning). --- # nLight.fit token management Long-lived API tokens are minted in the app, one per client, and revoked from the same place. Claude can skip this entirely and connect over [OAuth](/docs/mcp#connecting-over-oauth). Everyone else — Cursor, Claude Code with a header, a custom client — needs a token from here. ## Mint, list, revoke Open **Settings → MCP / CLI Access**. 1. Give the token a name you will recognise later (`cursor`, `claude-code`). 2. Generate it. The plaintext is shown **once**. It is stored hashed, so it cannot be displayed again. 3. Paste it into the client snippet on that same page, or into the config in [client setup](/docs/clients). 4. Revoke it from the list when you stop using that client, or if you think it leaked. Each token is independent. Revoking one integration does not disturb the others, and the last-used timestamp tells you which one is actually being called. Connectors you approved over OAuth appear in the same list, under the name of the application that asked. Revoking one disconnects that client on its next request. ## What you are granting A token minted here carries `read`. That is every MCP tool today. Nothing on this endpoint accepts `write`, and an OAuth consent screen cannot issue it either. See [authentication](/docs/authentication). ## Storing the token Treat it like a password with no expiry. Put it in your client's configuration or an environment variable; keep it out of source control, screenshots and shell history files that sync. If you are unsure whether one leaked, revoke it. Minting a replacement takes a few seconds and the old one dies on the next request. ## Related - [Authentication](/docs/authentication) — token format, scopes and storage - [Client setup](/docs/clients) — wiring the token into an MCP client --- # nLight.fit API versioning and deprecation A client — especially an automated one — needs to know two things before it commits to an integration: which version it is talking to, and how it would find out that version is going away. Both are answered by response headers, so neither requires reading this page at runtime. ## Which version answered Every response carries the version that served it. ```http Api-Version: 2026-08-22 ``` The version is a date rather than a number. The API is additive: new actions, tools and fields appear without a version change, because adding something breaks nothing. The date advances only when a change would break a client that was written against the previous behaviour, and it names the day that change took effect. A `/api/v1/*` alias exists for tooling that insists on a path segment — `POST /api/v1/mcp` reaches the same handler as `POST /api/mcp`. It is a convenience, not a second version; there is no second version to reach. ## What counts as breaking Treated as breaking, and therefore version-advancing: - Removing an endpoint, an `action` value, or an MCP tool. - Removing a field from a response, or changing its type. - Making a previously optional request field required. - Narrowing what an existing field will accept. - Changing an authentication or scope requirement. Not treated as breaking, and shipped without notice: - Adding an endpoint, an action, a tool or a response field. - Adding an optional request parameter. - Widening what a field accepts. - Changing the *content* of a computed value — a correlation strengthening, a summary being rewritten. The shape is the contract; the numbers are the product. MCP protocol revisions are negotiated separately and are not covered by this policy. The server states which revisions it speaks, and refuses anything else with the supported list attached. See [errors and limits](/docs/errors). ## How deprecation is signalled Deprecation happens in two stages, and each has its own header. **Stage one: deprecated but working.** The endpoint still responds exactly as before. It gains an [RFC 9745](https://www.rfc-editor.org/rfc/rfc9745.html) `Deprecation` header carrying the moment deprecation began, and a `Link` pointing here. ```http Deprecation: @1788134399 Link: ; rel="deprecation"; type="text/html" ``` The value is a structured-field date: `@` followed by whole seconds since the Unix epoch. `@1788134399` is 2026-08-22. **Stage two: a retirement date is fixed.** A [RFC 8594](https://www.rfc-editor.org/rfc/rfc8594.html) `Sunset` header is added, giving the moment the endpoint stops responding. It uses the older HTTP-date format — the two headers deliberately differ, and RFC 9745 says so explicitly. ```http Deprecation: @1788134399 Sunset: Sun, 20 Feb 2027 23:59:59 GMT Link: ; rel="deprecation"; type="text/html" ``` Where a replacement exists, it is named too: ```http Link: ; rel="successor-version" ``` The sunset date is never earlier than the deprecation date. ## The notice you get - **At least 90 days** between the `Deprecation` header first appearing and the `Sunset` header naming a date. - **At least 180 days** between a `Sunset` date being published and the endpoint ceasing to respond. Nothing is removed without both headers having been served first. If you have not seen a `Deprecation` header on an endpoint, it is not going away in the next 180 days. Every deprecation is also recorded in the [changelog](/docs/changelog), and the machine-readable version of this policy is in the `x-deprecation-policy` block of the [OpenAPI document](https://nlight.fit/openapi.json). ## Reading the headers Both `Deprecation` and `Sunset` are listed in `Access-Control-Expose-Headers`, so a browser-based client can read them cross-origin. A client that wants to notice deprecation without polling this page should check for the presence of `Deprecation` on any response and surface it. ## Nothing is deprecated today As of `2026-08-22`, no endpoint, action or tool carries a `Deprecation` header. The mechanism above is in place so that when something is deprecated, the signal is already something clients are reading — a policy announced at the same moment as the first deprecation is a policy nobody had time to act on. --- # nLight.fit errors and limits ## HTTP status codes | Status | When | What to do | | --- | --- | --- | | `200` | The message was handled. A tool that itself failed still returns 200 with `isError: true` in the result. | Read the result. | | `202` | The message was a notification (no `id`), so there is no response body. | Nothing. | | `400` | Malformed JSON, a batch array, an unknown method, bad tool arguments, an unsupported protocol version, or routing headers that disagree with the body. | Fix the request. The JSON-RPC error code says which. | | `401` | No credential, or one that is unknown, revoked or expired. Response carries `WWW-Authenticate: Bearer`. | Mint a new token. | | `403` | The token is valid but lacks the required scope, or the request carried a browser `Origin` header. | Use a token with `read` scope; do not call this endpoint from a browser. | | `405` | The request was not a POST. Response carries `Allow: POST, OPTIONS`. | MCP is POST-only. A `405` to a `GET` is what the Streamable HTTP transport prescribes for a server offering no server-to-client event stream; this one is stateless and has nothing to push. | | `429` | Rate limit exceeded. Response carries `Retry-After` in seconds. | Back off for the stated interval. | | `500` | Server-side failure. The message is deliberately generic. | Retry; if it persists it is not something the caller can fix. | | `503` | The data store could not be reached. | Retry with backoff. | ## JSON-RPC error codes | Code | Meaning | | --- | --- | | `-32700` | Parse error — the body was not valid JSON. | | `-32600` | Invalid request — not a JSON-RPC message, a batch, or an unsupported protocol version. | | `-32601` | Method not found. | | `-32602` | Invalid params — missing `params.name`, or a tool that is not exposed. | | `-32603` | Internal error. | | `-32020` | Header mismatch — `Mcp-Method` or `Mcp-Name` disagreed with the body, or was absent when the declared protocol version requires it. | | `-32001` | Authentication required — no credential was presented, or the one presented was rejected. Travels with HTTP 401 and a `WWW-Authenticate` header naming the authorization server. | | `-32002` | Forbidden — the credential is valid but lacks the scope the method needs, or the request carried a browser `Origin` and asked for member data. Travels with HTTP 403. | Handle `-32700` as one of two shapes. A request that declares a content type other than `application/json` reaches the server and is answered with the envelope above. A request that declares `application/json` and is not valid JSON is refused by the platform before this API sees it: **HTTP 400 with an empty body and no `Content-Type`**. Treat an empty `400` as a parse failure. ## Rate limits The MCP endpoint allows **120 requests per 60 seconds**, counted per user rather than per token. Each JSON-RPC message is one HTTP request, so a multi-step agent run consumes calls quickly; the limit is set to accommodate that while still bounding a runaway loop. The stated number is the ceiling to design against. It is not a precise meter, so do not build anything that depends on exceeding it. ### Rate limit headers You do not have to wait for a `429` to learn where you stand. Every response carries `RateLimit-Policy` describing the quota, and every response that spends quota also carries `RateLimit` describing what is left of it. Send a credential and every request is counted, so every response reports both. Send none and `tools/list` is still counted, while the negotiation methods (`initialize`, `server/discover`, `ping`) are answered without touching the limiter and carry the policy alone. | Header | Example | Meaning | | --- | --- | --- | | `RateLimit-Policy` | `"mcp";q=120;w=60` | The fixed shape of the quota: `q` requests per `w` seconds. Stable across responses. | | `RateLimit` | `"mcp";r=118;t=47` | Live state: `r` requests remaining, `t` seconds until the window refreshes. | | `RateLimit-Limit` | `120` | The same quota in the superseded three-header form, sent for client compatibility. | | `RateLimit-Remaining` | `118` | Requests left in this window. | | `RateLimit-Reset` | `47` | Seconds until the window refreshes. | | `Retry-After` | `47` | Sent only on a `429`. Seconds to wait before retrying. | `RateLimit` and `RateLimit-Policy` follow [draft-ietf-httpapi-ratelimit-headers](https://datatracker.ietf.org/doc/draft-ietf-httpapi-ratelimit-headers/). The three-header form is the earlier draft, kept because most existing clients read it. All of them are listed in `Access-Control-Expose-Headers`, so a browser-based client can read them cross-origin. ## Version and lifecycle headers | Header | Meaning | | --- | --- | | `Api-Version` | The API version that served the response. | | `Link` | `service-desc` points to the OpenAPI document, `service-doc` to the developer documentation, `version-history` to the versioning policy. | | `Deprecation` | Present only on a deprecated resource. An RFC 9745 structured field date. | | `Sunset` | Present only when a retirement date is fixed. An RFC 8594 HTTP-date. | Nothing is deprecated today. See [versioning and deprecation](/docs/versioning) for the notice periods these headers promise. ## Result size A single tool result is capped at 100,000 characters, roughly 25k tokens. Beyond that the payload is truncated and an explicit `[TRUNCATED: ...]` marker is appended, so a clipped series can never read as a complete one. If you hit it, narrow the date window or name fewer metrics. ## Protocol versions Send your revision in the `MCP-Protocol-Version` header. Supported, newest first: - `2026-07-28` - `2025-11-25` - `2025-06-18` - `2025-03-26` An unrecognised version is refused with a `400` that carries the full supported list in `error.data.supported`, so a client can pick one instead of guessing. Omitting the header entirely is read as `2025-03-26`, the last revision that predates the header. From `2026-07-28` onward, every request must also carry `Mcp-Method`, and `Mcp-Name` when the message addresses something (`tools/call`). The server checks them against the body and rejects disagreement, so an intermediary cannot route on one operation while the server performs another. ## Scopes Tokens carry explicit scopes: `read`, `write`. They are not cumulative — a `write` token does not imply `read`. Every MCP method today requires `read`, including `initialize` and `ping`. --- # Security and privacy This page describes the security contract: what is protected, how, and what is not. It does not describe internal defensive heuristics, which would be counterproductive to publish. ## Accounts Passwords are hashed with **bcrypt at 12 rounds** and compared in a timing-safe way. Only the hash is stored; the plaintext is never written anywhere. Sessions use signed tokens with a bounded lifetime. Change your password or use **Sign out other devices** in Settings and every other session dies on the next request. They are still not used as machine credentials: `/api/mcp` accepts only an API token. See [authentication](/docs/authentication). ## Encryption **A specific set of profile fields is encrypted at rest** with AES-256-GCM under a per-value random initialization vector, so identical inputs do not produce identical ciphertext. That set is your display name and seventeen profile fields: age, height, goal weight, goal, injuries, allergies, your stated commitment, what has worked and failed before, free-text notes, and the seven Mission Statement fields. Your WHOOP access and refresh tokens are encrypted the same way. **Nothing else is.** In plain terms: | Stored in the clear | | | --- | --- | | Lab results | Every marker and value from an uploaded bloodwork report | | Methylation results | Your genotypes | | Coach conversations | The full transcript of every chat | | Progress pictures | The image data itself | | Ledger data | Push-ups, meals, weights, habit checkboxes | | Daily summaries and learnings | Generated prose about your patterns | | Your username | | The database itself is encrypted at rest by the provider, and access to it is limited. Application-layer encryption — the kind that survives a database compromise — covers the eighteen fields named above and your wearable tokens, and nothing more. If you are uploading a genome or a bloodwork panel, assume it is protected by database access control rather than by field-level cryptography. Reads fail closed: a value that cannot be decrypted is withheld rather than served as raw ciphertext. ## Machine credentials API tokens for the [MCP server](/docs/mcp) are stored only as SHA-256 hashes, carry explicit non-cumulative scopes, and are checked against the database on every call — so revocation takes effect on the very next request. Full detail in [authentication](/docs/authentication). The MCP endpoint additionally refuses any request carrying a browser `Origin` header, which is what the MCP transport specification asks for to close the DNS-rebinding case. ## AI safety **Prompt injection defense.** Input to the AI endpoints is screened before it reaches a model, and suspicious content is either sanitized or blocked. This matters more here than in a typical chat product because the coach reads text you did not necessarily write — imported CSV content, extracted lab-report text, meal notes. **Length and history bounds.** Message length, conversation depth and total conversation size are all capped, so a single request cannot exhaust the context window. **Your data is not training data.** Content sent to model providers is processed to answer your request. It is not used to train models, and it is not shared with anyone else. ## Third parties | Service | Receives | Why | | --- | --- | --- | | MongoDB Atlas | Everything, as the system of record | Storage | | xAI | Coaching context for the current request; lab and methylation *text* during extraction | Chat, voice, extraction | | VoyageAI | Day and summary text | Embeddings and reranking | | WHOOP | Nothing. Data flows inbound only | Wearable sync | | Vercel | Request metadata and logs | Hosting | Two things deliberately never leave your device or your account: - **Lab and methylation PDFs.** Text extraction happens in your browser. Only the extracted text is sent for parsing, so the file — with its name, date of birth and provider header — stays local. - **Progress pictures.** Stored against your account and returned only to you. Not sent to any model, not embedded, and not readable by any MCP tool. Stored unencrypted; see the table above. ## Infrastructure HTTPS everywhere, enforced by the platform. Rate limits per user and per action. Circuit breakers that fail fast when an upstream service degrades rather than hanging your request. Schema validation on every API request, including sanitization that rejects MongoDB operators in field names. Structured logging with semantic event names, which is what makes an anomaly visible. ## What is not protected Being straight about the limits is more useful than a longer list of controls. **Rate limits are not a hard quota.** They are sized to bound abuse rather than to meter usage precisely, so treat a published limit as the intended ceiling rather than a guarantee. **API tokens do not expire.** That is safe only because revocation is immediate. A token you have forgotten about is a token that still works — list them periodically and revoke what you no longer use. **Health documents are not field-level encrypted.** Lab results, methylation results, progress pictures and chat transcripts sit in the clear inside the database. See the table above. **Ledger data is readable by anyone with database access.** See the encryption section above. The set of people with that access is very small, but it is not zero. **This is early-access software.** It is built carefully and tested, but it has not been through an external security audit. ## Your data **Export.** CSV export of your full ledger over any range, at any time, without asking anyone. **Deletion.** Delete the account from Settings (username + password). The record and every user-scoped collection go with it. **Ownership.** It is your data. It is not sold, not shared with advertisers, and not used to train anything. ## Reporting a problem If you find a security issue, report it privately rather than publicly, and give a reasonable window for a fix before disclosure. Write to `Austin@nLight.fit`. ## Related - [Authentication](/docs/authentication) — token format, scopes, revocation - [Data model](/docs/data-model) — what is stored and where --- # Data model What is stored, in what shape, and why. Useful if you are reasoning about the platform from the outside — or if you are an agent trying to understand what a tool result actually represents. ## The shape of a day The atomic unit is one user, one date: ```javascript { userId: "…", dateKey: "2026-08-01", // YYYY-MM-DD, the natural key data: { wakeAt7am: true, takeVitamins: true, weight: 178.4, pushUps: 60, breakfast: "eggs, oats", whoopRecovery: 71, whoopHrv: 88.2, // …any custom column, keyed by its id } } ``` Two things are worth drawing out. **Keys inside `data` are canonical metric ids.** `whoopHrv`, not "HRV". They come from the [metric registry](/docs/metrics), which is the same source of truth the API, the correlation engine and the AI tools read. WHOOP fields sit in the same object as fields you typed — there is no separate wearable record to join. **Custom columns live in the same object.** A custom column is not a second-class citizen stored elsewhere; it is a key in `data` with an entry in your column settings describing it. ## Collections | Collection | Holds | | --- | --- | | `routineData` | Tracked days, one document per user per date, with embeddings | | `routineSummaries` | Narrative rollups: weekly, monthly, quarterly, yearly, lab and methylation interpretations, mission analysis, correlation narratives | | `users` | Accounts, hashed passwords, and the profile — sensitive fields encrypted | | `userLearnings` | Insights extracted from conversation, embedded and scored | | `columnSettings` | Column definitions, order, visibility, custom column metadata | | `columnWidths` | Per-column display widths | | `dailyFeatures` | The feature store — normalized values and z-scores per day | | `apiTokens` | Hashed machine credentials with scopes and revocation state | | `labResults` | Bloodwork panels and extracted markers | | `methylationResults` | The one-time genetic profile | ## Derived artifacts Several things are computed rather than entered, and stored so they can be read cheaply. **Feature store.** 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. Stamped with the registry version, so a change to a metric's definition can trigger a targeted rebuild rather than a silent stale read. **Correlation edges.** Relationships as graph edges with coefficient, partial coefficient, lag, p-value, sample size, tier and provenance. See [analytics](/docs/analytics). **User facts.** All-time aggregates per metric — extremes, totals, streaks, rolling averages — derived from the registry rather than a hand-maintained field list. **Insights.** The precomputed statistical models: readiness, goal ETA, changepoints, dose-response, and the rest. **Summaries.** Narrative rollups, embedded alongside days so retrieval can find a period as a unit. ## What is encrypted Sensitive profile fields in `users` are encrypted with AES-256-GCM. Ledger rows, lab results, methylation results, coach conversations, progress pictures and learnings are not. The reasoning is in [security and privacy](/docs/security#encryption). ## Retrieval indexes Alongside conventional indexes on `userId` and `dateKey`, the database maintains a vector index over day and summary embeddings and full-text indexes over the same content. A question runs both, the results are fused, and the survivors are reranked. Neither alone is sufficient: vector search misses exact terms, text search misses paraphrase. ## Consistency The database is the single source of truth. The browser keeps essentially no cache of your data, every edit is written immediately, and every read comes from the database. Server-side caching exists for expensive computations, with a bounded TTL, and is invalidated for your account whenever you write. So a figure the coach quotes is either fresh or was computed against the full history by a background worker — never a stale value from a browser that has been open since yesterday. ## Reading it from outside The [MCP server](/docs/mcp) exposes most of this read-only. `get_day_details` returns a single day, `get_daily_features` the feature matrix, `get_correlation_edges` the graph, `get_user_facts` the aggregates, `get_insights` the models, and `get_summaries` the narratives. Full schemas in the [tool reference](/docs/mcp-tools). Not exposed: raw account records, password or token hashes, and progress pictures. ## Related - [Metric reference](/docs/metrics) — every field in `data` - [Security and privacy](/docs/security) — encryption and access - [Analytics](/docs/analytics) — how the derived artifacts are produced --- # Changelog Platform changes that affect what you can do. Internal refactors, performance work and dependency updates are not listed. ## August 2026 — anyone can create an account Signup is open. There is no waitlist, no approval step and no card. - **Register and start.** A username and a password at [nlight.fit/app](/app) creates the account and signs you in. New accounts are on the free plan. - **Free is a real tier, not a trial.** The whole tracking product — ledger, WHOOP, labs, genetics, CSV, charts, the nightly correlation engine — stays free and unmetered. The coach runs on the same `grok-4.6` as Member, thinking over your last 60 days at high reasoning. Member raises that to your full record at maximum depth. See [membership](/docs/membership). - **Over the monthly coach allowance, answers get cheaper rather than refused.** Reasoning drops to the floor and the context narrows. Nothing tells you to come back next month. - **Voice now has a monthly budget** — about 10 minutes free, 60 for Members — enforced when the session starts rather than partway through a sentence. - **Everyone who already had an account has a free year of Member**, starting the day open signup shipped. - **No password reset yet.** There is no email on file to send one to, so keep your password somewhere durable. ## August 2026 — security and privacy hardening - **Delete your account from Settings.** Username + password confirmation erases the profile, ledger, labs, genetics, photos, chats, tokens and related collections. Stripe payment records are kept as a legal/tax record. - **Honest coverage.** The [security](/docs/security) and [privacy](/privacy) pages, the landing Trust section, and the login privacy note now say exactly which fields are encrypted at rest — and that labs, genetics, photos and chat transcripts are not. - **Safer failures.** Unreadable encrypted values are withheld instead of being shown or sent to models as ciphertext. Revoking an MCP token also kills the matching OAuth refresh token. - **Sessions can be killed.** Change your password or use **Sign out other devices** in Settings and every other session ends on the next request. The MCP server accepts only an API token, not a browser session. ## August 2026 — docs assistant A public, docs-only assistant now sits in the bottom-right corner of the documentation site and the marketing pages. - **Find, curate, navigate.** Ask a general question and it narrows to the best published pages, with heading-level links. It will not talk about anything except nLight.fit documentation. Following a link keeps the chat in that tab so you can keep opening the other pages it suggested. - **Readable answers.** Replies render GitHub-flavored Markdown — tables, headings, lists — instead of raw pipes. Free vs Member comparisons use a compact three-column table. - **Membership, explained.** It can walk through why the Member plan is $50/year — higher reasoning over full history — with the same concrete examples as the [membership](/docs/membership) page. - **Locked down and rate limited.** Off-topic questions, jailbreaks and secret-fishing are refused without a model call. Public IP limits cover a burst window, an hour and a day. See [docs assistant](/docs/docs-assistant). ## August 2026 — Member is $50/year List price moved from $30 to $50 per year. See [membership](/docs/membership) for what the upgrade buys. ## August 2026 — Member, $30/year The paid intelligence tier is now a path you can take, not only a billing rail. Price later moved to $50/year (see the entry above). - **Pricing on the landing page.** Free (early access) and Member sit next to each other. Become a Member goes to `/app?upgrade=1` so checkout is attached to your signed-in account. A public Stripe Payment Link is never shown. - **Upgrade in the app.** Settings opens on a Membership panel. The same panel is in your Profile. Free accounts also see an Upgrade chip in the top-right account bar. - **What membership buys.** Frontier-model intelligence on your full history, highest reasoning, mission analysis, and expanded voice. See [membership](/docs/membership). - **Mission analysis upgrade path.** A free account that asks for Mission Analysis sees a Member CTA that opens Settings rather than a generic retry error. ## August 2026 — an API you can find and try Everything here is for the client integrating against nLight.fit rather than for the person using it. - **A sandbox that needs no account.** `POST /api/sandbox` speaks the same MCP protocol with the same eighteen tool schemas as the production server, answered from a synthetic member record — a complete, credential-free environment with a populated history to develop against. Every response is marked as synthetic. See [sandbox](/docs/sandbox). - **An OpenAPI specification** at [`/openapi.json`](https://nlight.fit/openapi.json), describing the REST and MCP surface including the auth, rate-limit and versioning conventions. - **Discovery documents at their conventional paths** — an [RFC 9727 API catalog](https://nlight.fit/.well-known/api-catalog) and an [MCP server manifest](https://nlight.fit/.well-known/mcp.json) — so a client can find the API without being told where to look. - **A developer hub** at [`/developers`](/docs/developers) listing every resource with its URL. - **Rate limits reported, not just enforced.** Every response now carries `RateLimit` and `RateLimit-Policy`, so a client can pace itself instead of discovering the limit by hitting it. A `429` adds `Retry-After`. See [errors and limits](/docs/errors). - **A versioning and deprecation policy** with the headers behind it: `Api-Version` on every response, and `Deprecation` and `Sunset` when something is on its way out, with stated notice periods. Nothing is deprecated today. See [versioning and deprecation](/docs/versioning). ## August 2026 — MCP server External AI clients can now read your record. - **One-click connection for Claude.** Paste `https://nlight.fit/api/mcp` into Settings → Connectors → Add custom connector, sign in, approve a screen that says exactly what will be readable. No token to mint, no config file to edit, and it works the same on Desktop, on the web and on mobile. The connection is read-only and cannot be made otherwise — the server will not issue a write scope to a connector at all. See [client setup](/docs/clients). - **MCP server** at `POST /api/mcp`, stateless JSON-RPC over HTTP, with eighteen read-only tools. Claude Code, Claude Desktop and Cursor all connect directly. See [MCP server](/docs/mcp). - **Your goals and constraints over MCP.** `get_profile` gives an external client your Mission Statement and the injuries, allergies and exclusions any recommendation has to respect. Until now a connected client could read your genetics, your bloodwork and your recovery — and compose a training or supplement plan without any way to see an injury on file. Your name and your free-form notes are deliberately never exposed; see [what is not exposed](/docs/mcp#what-is-not-exposed). - **Supplements over MCP.** `get_supplements` is now published to external clients, and the genetics tool carries a cross-check against it: `get_methylation_data` states which of its recommended compounds you already take, which are genuinely still open, and which item in your current stack your own variants argue against. Until now the half that recommends was reachable over MCP and the half that checks was not, so a client could suggest adding something you already take. - **Warehouse tools** — six tools available only over MCP that return raw analytical evidence rather than a precomputed verdict: the daily feature matrix, the correlation graph as structured edges, all-time facts, statistical models and narrative summaries. These let a client test a hypothesis the nightly engine never considered. - **API tokens** — revocable machine credentials with explicit `read` and `write` scopes, stored as hashes, managed from Settings → MCP / CLI Access. See [token management](/docs/token-scripts). - **This documentation site**, with the tool and metric references generated directly from source so they cannot drift. ## August 2026 — statistical honesty Changes to what the platform is willing to state, and how plainly it admits what it does not know. - **Every tool result says how current it is.** A tool answering over MCP now stamps a `dataThrough` date — the day your record actually runs through, not the day the answer was computed. Live ledger figures and nightly-computed ones used to be quotable side by side as though they described the same moment, which is how a client ends up presenting a stale number as today's. See [how current is this result](/docs/mcp#how-current-is-this-result). - **Sparsely-logged habits no longer masquerade as recovery drivers.** A behaviour has to be recorded on at least 30% of the days being modelled before it earns a slot in your personal recovery formula. Below that, treating every unlogged day as "didn't do it" produced a coefficient measuring whether you filled in the row rather than whether the habit worked — which is how "prior-day vitamins: −9.9 recovery points" reached the coach from a habit logged one day in seven. - **Confounded terms are labelled where they appear.** A recovery-formula term the correlation engine judged confounded now reads as such wherever it is stated, so a real association is not presented as a lever you can pull. See [your recovery formula](/docs/analytics#your-recovery-formula). - **Beliefs are scored against what could actually be tested.** When your stated beliefs are cross-examined against your statistics, only measurable claims — observed patterns and behavioural assertions — count toward the result. A preference or a plan asserts nothing a correlation can check, and counting it as "untested" made the check read as far weaker than it is. - **Metrics declare what they are for.** Each metric now carries a purpose: an analytical signal, or something tracked for your benefit that was never a modelling input. This is what separates "no relationship was found" from "this was never tested" — claims of very different strength that used to look identical. See [purpose](/docs/concepts#purpose). - **Unreadable genotypes are reported as unknown, not normal.** A gene whose genotype could not be parsed out of an uploaded report is no longer grouped with the genes confirmed to carry no variant. See [unparsed genotypes](/docs/methylation#unparsed-genotypes-are-unknown-never-normal). ## Mid 2026 — deeper analysis - **Unified correlation engine** spanning habits, nutrition, WHOOP, bloodwork and genetics in one graph, with partial-correlation controls, autocorrelation-adjusted significance and multiple-testing correction. - **On-demand relationship analysis** for any two metrics, applying the same statistical guards, always labelled a hypothesis. - **Insights engine** — readiness scoring, lead-lag detection, goal ETA with Monte Carlo confidence bands, streak survival curves, training load, HRV baseline, dose-response curves, changepoint detection. - **User fact index** derived from the metric registry, which fixed a real class of error: personal records that only reflected the most recent thirty days. - **Metric registry** as the single source of truth for metric semantics, replacing definitions that had been copy-pasted across five modules and drifted apart. ## Early 2026 — health modules - **Bloodwork.** Upload lab PDFs, extract markers, track trends against reference ranges, correlate against daily behaviour over biologically plausible windows. - **Methylation genetics.** One-time genetic profile with pathway view, clusters, genetically predicted watch markers cross-linked to your labs, and priors that get tested rather than asserted. - **WHOOP webhooks**, so recovery scores appear within minutes instead of at the next scheduled pull. - **Data-quality gating** for WHOOP calibration periods and nights with sensor gaps — visible in the ledger, excluded from analysis. - **Mission Statement** — one goal, three commitments, with progress and causality analysis. ## Late 2025 — the coach - **Context Engine 2.0** — temporal awareness, momentum detection, lifecycle stage, profile gap discovery. - **Smart payload** — question classification driving model choice, reasoning effort and context size. - **Learnings system** — bidirectional extraction, semantic deduplication, decay, consolidation, bounded storage. - **Voice agent** over a realtime speech model, with unified, chat, data-entry and onboarding modes. - **Prompt injection defense** on the AI endpoints. - **Hybrid retrieval** — vector search and full-text search fused and reranked. ## Related - [Overview](/docs/overview) — what the platform does today ---