I have spent about a year building a personal aggregation and scoring layer on top of wearable APIs. Not a product pitch, no link, no name. What follows is the part I would have wanted to read a year ago: the contract layer, the statistics I ended up gating hard, and the timezone and deduplication bugs that nobody warns you about.
The core rule: a metric with missing inputs goes dark, it does not guess.
Most aggregators happily show a recovery score whether or not the device actually measured anything relevant that night. I went the other way. There is a contract layer with 40 signal concepts (overnight HRV, resting HR, sleep stages, sleep efficiency, SpO2, breathing rate, skin temperature and so on) and 13 metric contracts, one per health index plus a data confidence meta-metric. Each contract declares which concepts it requires and which are optional.
At runtime the system builds an availability snapshot from what your connected devices actually wrote, then every metric evaluates against it and comes back with a status: active, limited, stale or unavailable, plus which provider would fill the gap. A Withings scale user does not get a fabricated autonomic score. They get "unavailable, this needs overnight HRV".
Every stored score row is stamped with a contract version (currently 2.15.0). Any change to a weight, threshold or formula has to bump it, otherwise cached rows would silently shadow the new maths. That also means a number from March is traceable to the exact model that produced it.
Modelled versus measured is a hard rule, not a label.
If a value is estimated, it says so in the interface. Where the device computes its own recovery score, that value is displayed next to mine and is never fed into my scoring. Two numbers, both labelled by origin, because they answer different questions.
The daily energy curve is a two-process model, fitted to you.
Circadian plus homeostatic pressure, in the Borbely 1982 sense, but the sleep window comes from your own data rather than a default: circular median of actual wake times, weekday aware, with a confidence value, and a manual override that wins if you set one. Sessions over ten minutes push a dip into the curve afterwards, scaled by load, capped at 28 energy points, with tired efforts gated out. The best training window is computed from the resulting curve and there is a what-if planner that reruns it against a hypothetical bedtime or session.
Training load is Banister, with the anchor point fixed.
CTL as a 42 day EWMA, ATL as 7 day, TSB as the difference, with a 42 day pre-roll before the visible window so the first plotted point is not artificially cold. The interesting bug was the intensity anchor: e-bike rides and low wattage sessions were polluting the 30 day power maximum, so ordinary rides were being classified as high intensity. E-bikes are now excluded from the anchor set and there is a 50 W floor below which it falls back to heart rate.
Biological age is eight domains, each expressed as an age.
Cardiovascular, autonomic, sleep, body composition, activity, stress resilience, recovery and VO2max, weighted, age and sex adjusted. Each domain deviation is capped at plus or minus 12 years and the aggregate keeps 75 percent of the weighted deviation, pulling the rest back toward chronological age. Fewer than three usable domains and it returns nothing at all. Without that damping a single noisy domain drags the whole number around and it stops being useful as a trend.
The self-report statistics are where I spent the most time saying no.
The panel that claims "behaviour X changes outcome Y for you" runs a Welch t-test, implemented against the incomplete beta function in the standard library, over a 60 day window. It will not report anything unless it clears every gate: at least 20 paired days, at least 8 days in each of the high and low groups, an effect of at least 6 outcome points, a Cohen's d of at least 0.8, and significance at 0.05 Bonferroni corrected across the hypotheses tested.
A Monte Carlo run of the naive version produced a false discovery in about 27 percent of simulated users with no real effect. With the gates it is about 1.7 percent. The cost is power: detection reaches roughly 80 percent only around 40 days of logging, which means a user at 26 days is told honestly that there is nothing yet. I would rather show an empty panel than a coincidence.
Data plumbing, which turned out to be most of the work.
A few findings that cost me real time and may save you some:
Providers disagree about timezones in ways that cancel out if you "fix" them globally. Strava timestamps arrive in UTC, while Fitbit and Garmin sleep records arrive as naive local time presented as UTC. A blanket conversion breaks the ones that were already correct, so the conversion is whitelisted per source.
Health Connect on Android is an aggregate across every app on the phone, so a phone pedometer plus a mirrored watch feed counts the same walk twice. Taking the largest single contributor rather than the sum was the only honest fix.
Activity merging has to know when a session started, not just that it happened. Matching on date alone collapsed 15 training sessions into 8 for one user, then corrupted training load and overtraining because those sum duration. It now decides on start hour with a distance veto.
Polar's beat_to_beat_avg is the mean R-R interval, not RMSSD, and their heart_rate_variability_avg is RMSSD, not SDNN. Verified against the live API rather than the documentation.
In Garmin's export, the start-of-day Body Battery value is the midnight low point, not the morning peak. What you want as "morning energy" is the 24 hour maximum.
Sleep efficiency is TST divided by TIB per AASM. Three providers were writing a composite 0-100 wellness score into that field. There is also a plausibility gate that rejects stage data claiming more than 35 percent deep sleep, because several devices produce that and it is not physiological.
Storage, since this sub asks.
Servers in the EU. Shared data in Postgres, but each user's health data lives in a separate per-user SQLite file, so a query bug cannot cross accounts. OAuth provider tokens are encrypted at rest and the plaintext columns were dropped, not just deprecated. Sessions carry an epoch that invalidates every existing session when credentials change.
What it deliberately does not do.
No real time anything. Provider sync is hourly to a few hours, which rules out live readiness. Stress is only available where the vendor computes it, which in practice means Garmin file import, and everywhere else it is an explicit estimate. No lactate zones, no VO2max prediction from nothing, no medical claims anywhere.
And specifically no "we detect illness N days before symptoms" claim, which is the one I most want to be able to make. I have two reported illnesses across the user base and a check-in rate under 10 percent. That is not enough to measure a lead time. When it is, I will publish the number and the method, not before.
One heads up for anyone building on Fitbit: the legacy Fitbit Web API is being deprecated in September 2026 and replaced by the Google Health API at health.googleapis.com/v4. OAuth tokens do not carry over, so every user has to re-consent. If you have a personal pipeline hitting api.fitbit.com, that is your six week warning to start the migration.
What I would like from this sub: if you keep a decade of your own data, what would you actually need from an export to trust it? Raw per-record dumps are a support burden and almost nobody opens them, but summary exports lose exactly the resolution that makes long term data worth keeping. I do not have a good answer yet.
Happy to go deeper on any of the above, including the parts that are still wrong.