My smart ring sucked, so I built my own Whoop

October 28, 2026

I wear a RingConn Gen 3 every day. The hardware is great. The app is not. The UI is horrible, the insights are pretty lackluster, and RingConn has no API, so nothing else can even read the ring. Bevel is the closest thing out there, but it's so cluttered.

All I wanted was one clean, minimal screen that tells me how I slept and how ready I am, plus an AI that actually reads my data. So I built my own. It's called Halo, a native iPhone app that talks to the ring over Bluetooth and turns its raw data into WHOOP and Oura style scores. The first version was on my phone about three hours after I started.

Halo's home screen in dark mode. A dotted green header says Good night, Pranav. Below it are three scores in a pixel font: Readiness 90 (Optimal), Strain 1.0 against a target of 14 to 17, and Sleep 91 for 7h 05m. Then a Morning brief card titled 'Best HRV in 60 days: keep today easy, push tomorrow', and a Logged list with 150 mg of caffeine and a heavy meal.
Halo's home screen (demo data)

Starting from OpenCircuit

I didn't crack the ring's protocol myself. Halo is built on top of OpenCircuit, an open source project that already reverse engineered how RingConn talks over Bluetooth. They found some wild stuff. The data isn't encrypted at all, the login is a challenge-response keyed on the ring's MAC address, and there's a hidden 100Hz raw optical sensor stream in there.

The catch is that OpenCircuit is a whole app, and its Bluetooth layer alone is 6,838 lines. I only needed the part that understands the ring.

So I kept OpenCircuitKit, the core that decodes the ring's messages, and wrote my own Bluetooth layer around it. It's one Swift file, 523 lines, about 13x smaller. It connects, logs in, pulls the history and streams live heart rate, and that's it.

fig 01

I only kept the part that understands the ring

Lines of Swift in each app's Bluetooth layer

  • OpenCircuit6,838
  • Halo523

13× smaller

show data
Bluetooth layerLines
OpenCircuit6,838
Halo523

Most of the early bugs were me reading the wrong bytes. My favourite was in OpenCircuit's scripts, which found records by checking for a first byte of 0x0c. That's actually the top byte of the timestamp, and it flips to 0x0d on November 28, 2026, so they'd have silently stopped reading history in about a month.

Staying awake all night

Once the ring was talking, I hit the real problem. iOS suspends apps overnight, and overnight is the whole reason I wear the thing.

The fix came from the ring. It sends a small heartbeat signal about once a minute, and with Bluetooth background mode on, iOS wakes the app for each one. So Halo just rides the ring's heartbeat, pulling down the night so far every 15 minutes. I was dreading a redesign around background tasks, and this let me skip all of it.

fig 02illustrative

The ring keeps waking the app up

iOS suspends Halo overnight, but each ring heartbeat wakes it for a moment. Every 15th one, Halo pulls the night so far.

  • ring heartbeat · ~every minute
  • Halo pulls history · every 15 min
23:0000:0001:0002:0003:0004:0005:0006:0007:00

zoomed in · one 15-minute window · the app wakes for each beat, then sleeps

ringapppull the night so far ↑02:0002:15

On first launch Halo also backfilled 60 days from Apple Health, so my baselines existed on day one. That matters more than it sounds, you'll see in a sec.

The ring stores one record every 150 seconds with heart rate, HRV, blood oxygen, breathing rate and motion. RingConn computes sleep stages and scores on its own servers, so if I wanted scores, I had to make them.

How the scores actually work

WHOOP and Oura are the industry leaders in recovery and sleep tracking, so I wanted my scores to work like theirs. Neither one publishes its formulas, but companies patent what they won't publish. So I researched it properly. I read 18 WHOOP patents, papers by both companies' own scientists, reverse engineering repos, and a project that had decompiled the scoring library inside Oura's app.

The biggest idea is that everything is compared to you. A resting heart rate of 50 means nothing by itself, only next to your normal. So Halo keeps a baseline for each metric and measures how far tonight is from your usual night to night swing.

fig 03illustrative

Same number, very different night

Both people wake up with HRV 20 ms above their average. What matters is how much they normally swing.

Person A · usually swings ±10 ms

tonight +20-60-300+30+60

2.0 usual swings above normalbetter than 98% of their nights

Person B · usually swings ±30 ms

tonight +20-60-300+30+60

0.7 usual swings above normalbetter than 75% of their nights

x axis: HRV vs personal average, ms · curves: each person's usual night-to-night spread

The sleep score turned out to be simpler than I expected. Oura's is a weighted average, so each part of your night counts for a fixed share of the score. How long you slept counts the most, then how restless you were, and then efficiency, REM, deep sleep, how fast you fell asleep and your timing all count equally.

fig 04

Oura's sleep score is a weighted average

Each part of your night counts for a fixed share of the score

  • Total sleep35%
  • Restfulness15%
  • Efficiency10%
  • REM sleep10%
  • Deep sleep10%
  • Time to fall asleep10%
  • Timing10%

Weights from a decompiled copy of Oura's scoring library; checked against 336 days of one person's scores (R² 0.9987).

show data
PartShare of score
Total sleep35%
Restfulness15%
Efficiency10%
REM sleep10%
Deep sleep10%
Time to fall asleep10%
Timing10%

To check it, someone took over 336 days of their own Oura scores and compared them to this formula. It matched almost perfectly (R² of 0.9987, where 1 would be a perfect match). It also showed I was being way too nice to myself. My first version gave full marks for total sleep at about 7.75 hours, but Oura only gives 100 at 9 hours.

fig 05

I was being way too nice to myself

Sleep duration score by hours slept

  • Oura
  • my first version
02550751003h4h5h6h7h8h9h10hmine: 100 at 7h 45mOura: 88100 at 9h

Oura's anchors from patent US11793454B2. Hover for exact scores.

show data
Hours sleptMy first versionOura
4h1858
5h4067
6h6375
7h8582
7h 45m10088
8h10090
9h100100
10h100100

Recovery is WHOOP's morning score, from 0 to 100%, for how ready your body is to take on a hard day. HRV, or heart rate variability, is the tiny variation in time between your heartbeats. When it's higher than your normal, your body's usually well rested.

Recovery is mostly HRV. Someone ran the numbers on 411 days of their own WHOOP data to see what actually moves the score. HRV did about half the work, how much you slept compared to what you needed did about a quarter, and resting heart rate barely mattered.

fig 06

Recovery is mostly HRV

Share of the day-to-day change in WHOOP recovery each input explains

  • HRV48–50%
  • Sleep vs need23–29%
  • Resting heart rate3–5%

One person, 411 days of WHOOP data. Ranges span two models fit to the same data (Shapley shares).

show data
InputShare
HRV48–50%
Sleep vs need23–29%
Resting heart rate3–5%

The shape matters too. Recovery follows an S-curve, which means it changes fast when you're close to your normal and flattens out at the extremes. A really great night and an insanely great night end up scoring about the same. On days the model hadn't seen, an S-curve explained 82% of the swings in their recovery, compared to 59% for a straight line.

fig 07

Recovery follows an S-curve

Fast change near your normal, flat at the extremes. A great night and an insanely great night score about the same.

  • S-curve · explains 82% of the variation
  • straight line · 59%
0255075100-3-2-10+1+2+3usual swings from normal →steepflatflat

S-curve is Halo's recovery map. Fit quality is hold-out R² from one person's 411 days of WHOOP data.

show data
Tonight vs normalS-curveStraight line
-322
-2619
-12436
05853
+18670
+29787
+399100

Strain is WHOOP's number for how hard your day was on your body, on a scale from 0 to 21. It adds up how long your heart rate stayed up and how high it went. The scale gets harder to climb the higher you go, so 18 to 19 takes way more than 4 to 5. A chill day lands under 10, and a hard workout pushes 14 to 17.

Strain needed a minimum. Your heart rate reserve is the gap between your resting and max heart rate, and Halo only counts time spent above 30% of it, which is about 84 bpm for me. An open source WHOOP clone that counted everything gave a day of doing nothing a strain of 11 to 12. With the minimum, a day sitting at my desk barely registers.

fig 08illustrative

Strain only counts above a minimum

Halo only counts time above 30% of your heart-rate reserve, about 84 bpm for me. Sitting at a desk barely registers.

  • heart rate
  • counts as strain
  • 84 bpm minimum
408012016006:0009:0012:0015:0018:0021:0000:00bpmworkout

Heart-rate reserve = max HR − resting HR. Without a minimum, an open-source WHOOP clone gave a day of doing nothing a strain of 11–12.

Halo's workout page for a run recorded on an Apple Watch. A heart-rate curve shaded by zone peaks at 155 bpm over about 45 minutes. Below it: duration 45 min, average and max 142 / 155 bpm, 467 calories and strain 9.7, then the start of a heart-rate zones breakdown.
Workouts come from my Apple Watch. This 45 minute run is a 9.7 (demo data)

One of my favourite finds was a WHOOP patent that just prints the formula for how much extra sleep you need after a hard day.

extra sleep (hours) = 1.7 / (1 + e^((17 − strain) / 3.5))

At a strain of 5.3 it gives exactly the 3.5 minutes from WHOOP's own API example. Patents come with worked examples like that, so I turned them into tests. Halo's engine tests are pinned to numbers from WHOOP's and Oura's own documents, so if my math drifts from theirs, a test fails.

fig 09

How much extra sleep a hard day costs

extra sleep (h) = 1.7 / (1 + e(17 − strain) / 3.5)

020406080036912151821strainminstrain 5.3→ 3.5 min

Formula printed in WHOOP patent US 12,318,226 B2. Strain 5.3 → 3.5 min matches WHOOP's own API example.

show data
StrainExtra sleep
00.8 min
5.33.5 min
87.2 min
1012.2 min
1219.7 min
1430.4 min
1751.0 min
1965.2 min
2177.3 min
Halo's sleep page for a night from 11:25 PM to 6:42 AM. Time asleep reads 7:05 in large pixel digits over a purple dotted band, with Score 91, Optimal, 96% efficient. Below is a hypnogram with Awake, REM, Light and Deep rows across the night, and the start of a stages breakdown showing 19 minutes awake.
The sleep page. Touch and hold the hypnogram to scrub through the night (demo data)

Letting my AI fill in the context

The other thing I wanted was for my AI agents to be part of it. My Grok bot has way more context about my life than any app does. It knows when I had drinks, a late meal or a rough day because I already tell it. So Halo syncs to a small server, Grok connects over MCP, and it logs that context into Halo on its own. Halo only calls something a real effect once it has 5 nights with it and 5 without, and the difference beats my normal noise.

fig 10

My AI fills in the context

I tell Grok about my day like I already do. It logs the context into Halo on its own.

  1. 1
    Mehad 3 drinks
  2. 2
    Grokalready knows my day, calls Halo over MCP
  3. 3
    Halo servertag · alcohol · 3 · fri night
  4. 4
    Halo appfri night · 3 drinks

Halo only calls a tag a real effect after 5 nights with it and 5 without, and only if the difference beats my normal noise.

Halo's Logged screen, with context from Grokbot and the user over the last 30 days grouped by the night it affects: stress 4/5 from a project deadline, 95 mg of coffee, 150 mg of cold brew at 3:30 PM, a heavy burger dinner at 8:45 PM and 3 drinks at 9:40 PM.Halo's Impacts screen comparing nights with and without each factor. Alcohol: HRV down 4.8 ms and resting heart rate up 2.2 bpm, from 8 nights with versus 28 without. Caffeine: deep sleep up 13 minutes, 9 nights versus 27. Meals are still learning at 1 of 5 nights.
What Grok logged, and what Halo learned from it (demo data)

Every morning Claude writes me a brief from the same math the app uses. Today's noticed my HRV averages 74.9 ms on weekends vs 89.4 on weekdays, so this Saturday's experiment is strain under 14 and in bed before 1:30.

A Halo morning brief headlined 'Best HRV in 60 days: keep today easy, push tomorrow'. The summary cites HRV of 60.1 ms, resting heart rate of 44 bpm and 77% recovery against a 66% median, and says 7h 05m was about 86% of an 8.3 hour sleep need. Below are Readiness 90, Strain 1.0 and Sleep 91, then an HRV chart from Oct 14 to 27 against the normal range.
A morning brief written by Claude (demo data)

With AI you can just build the thing you want now. I was stuck with an app I didn't like for a ring I wear every day. Now I have my own app, scored the way I want, talking to my own agents.

I think this is the first of many. There's a lot of stuff I use every day that I'd rather just build myself.