DEV Community

Cover image for The System Never Checked If You Slept. Ours Reads Your Pulse First.
Subarna Maity
Subarna Maity Subscriber

Posted on

The System Never Checked If You Slept. Ours Reads Your Pulse First.

Let's Start With Hackathon Scene

Main video is made by the Organizers of Hacktropica this my edited version on it

Right now, while you read this, your face is changing colour.

Not by much. Every heartbeat pushes a pulse of blood into the capillaries under your skin. Haemoglobin soaks up green light a little more than the tissue around it, so your cheeks darken and fade, darken and fade, roughly once a second. The change is far too small for anyone looking at you to notice.

A phone camera notices it anyway.

Robust blood pressure measurement from facial videos in diverse environments

You have never seen it happen. It has happened every second of your life.

Keep that in mind, because this story runs on three clocks, and they do not run at the same speed:

CLOCK 1  THE SCAN      10 seconds    a face, a front camera, a pulse
CLOCK 2  THE BUILD     36 hours      four people, one repo, one prize
CLOCK 3  THE READING   5 months      one line in the docs we never read
Enter fullscreen mode Exit fullscreen mode

Three horizontal bars labelled The Scan (10 seconds), The Build (36 hours) and The Reading (5 months later), the last one quoting Presage's docs:

The first clock is the product. The second is how it got made. The third is the reason this post exists, and it does not show up until near the end. When it does, it changes what the first two mean.

The System never checked if you slept

In Solo Leveling, a mysterious System hands Sung Jinwoo the same daily quest every single day. 100 push-ups, 100 sit-ups, 100 squats, a 10 km run. Didn't sleep? The System doesn't care. Nearly died yesterday? The System doesn't care.

Every fitness app is that System. It asks how you feel, lets you tap a smiley face, and then serves the plan it was always going to serve.

But your face is already telling the camera how you feel. Heart rate and breathing rate are sitting right there in the colour of your skin.

So the question is not "what workout should I do today?" It is:

What if the System measured you before it assigned the quest?

Why Its So Linked With Solo Leveling Anime

That is SoloFit ("Arise. Train. Conquer."), a React Native app that reads heart rate from the phone camera, turns it into a readiness score, and only then asks an LLM to write your week. Our team, Kolkatar Rosogollas, built it in 36 hours at HackTropica 2K26, and it won MLH: Best Use of Presage SDK.

Here is the whole pipeline, with the real numbers from the code:

Eight-step pipeline: 10 s video, S3 multipart upload, Presage rPPG, HR and RR, readiness 0-100, 3 bands, Gemini 2.5 Flash, 7 hard rules
1 scan -> 2 vitals -> 1 score -> 3 bands -> 7 days -> 7 rules

One thing up front, because it changes how you read everything below

SoloFit has two ways of getting your vitals, and you need to know which is which before any number in this post means anything.

  • Measured. The video goes to Presage, comes back as heart-rate and breathing-rate samples, and the result is tagged source: "presage_api". The scan screen shows a green badge.
  • Estimated. If anything fails (no API key, no video file, upload error, a 100-second timeout), the service does not throw. It returns randomly generated vitals: heart rate 65-95, breathing 13-20, tagged mock_no_key, mock_no_video or mock_api_fallback. The scan screen shows an amber badge that says "Estimated Vitals."

The fallback was a deliberate hackathon decision. The code comment says so plainly: Fallback is expected in some environments; keep this non-error in UI/logs. A demo that crashes on bad venue Wi-Fi is not a demo.

What that fallback does downstream is a different story, and the third clock is where I tell it. For now, remember the amber badge.

And one line that applies to the whole post: Presage's own documentation says its metrics are for general wellness and informational purposes only, have not been cleared by the FDA, and may not be used for medical diagnosis or treatment. SoloFit is a fitness app. It is not a medical device, and nothing below is medical advice.

What I Built

CLOCK 1 · THE SCAN · 10 SECONDS

[Daily Quest] Look at the camera for ten seconds. Reward: a plan that knows how you slept.

Most fitness apps give you a plan built from your goals and your equipment, and the plan is the same on the morning after a full night's sleep as it is the morning after an all-nighter. The README puts it in one line I still like: "Most fitness apps ask the user how they feel. Solofit measures it."

The app runs on three beats:

  • Measure, don't assume. A 3-second countdown, then a 10-second front-camera recording. Presage reads your heart rate and breathing rate from that video.
  • Reason, don't template. Those vitals, plus your profile (age, goals, equipment, injuries, diet, allergies, budget, cuisine), become a readiness score and a prompt. Gemini 2.5 Flash writes a 7-day workout and diet plan.
  • Adapt, don't repeat. Scan again tomorrow and the plan is rebuilt from tomorrow's body, not yesterday's.

Three real SoloFit screens: the vitals scan viewfinder, the dashboard with readiness, and the day 1 workout plan

Every screenshot in this post is the real app, running on web through Playwright with no API keys. That matters later, so remember it.

The rest of the app keeps the Solo Leveling mechanics: quests with XP, a global leaderboard, a community feed, a posture screen that uses MediaPipe, and exercise GIFs so nobody has to guess what a "Bulgarian split squat" is. It doesn't keep the Solo Leveling look. It had it for part of the weekend, and why it lost it is the best story from the event. That's in Hackathon Experience.

The stack: React Native on Expo SDK 54, Firebase Auth and Firestore for profiles, the Presage Physiology REST API for vitals, Gemini 2.5 Flash for plans, Groq for fast meal swaps, MongoDB Atlas behind an Express server for the leaderboard and community, and Zustand for state.

Three engineering decisions worth stealing

1. Never trust an LLM's JSON. Validate it against hard rules, repair it, then fall back to code.
The LLM does not get the last word on your workout. A validator does (more on the seven rules below). If the plan breaks a rule, the exact violation goes back to the model in a repair prompt. If the repair still breaks a rule, a deterministic plan builder takes over. The user always gets a valid week.

2. Route each call to the model that fits its latency budget.
Generating a whole week can take a while, and nobody minds. Swapping one meal while you stand in the kitchen has to feel instant. So meal swaps go to Groq's llama-3.1-8b-instant in JSON mode at temperature 0.2, and only fall back to Gemini if that fails. Plan generation goes the other way: Gemini first (with a backup key), then Groq's openai/gpt-oss-120b if Gemini is down.

3. Tag every data point with where it came from.
Every vitals object carries a source string. That one field is what lets the UI tell measured data apart from estimated data. It is also what made the problem in the third clock easy to find.

Demo

Solofit — Real-Time Adaptive AI Fitness Coach

A mobile fitness application that uses live biometric data to generate and continuously adapt personalised workout and diet plans — in real time, for every individual.


Table of Contents

  1. What is Solofit?
  2. The Problem We Solve
  3. Core Technology Stack
  4. Presage SmartSpectra — The Heart of Solofit
  5. Gemini AI — The Brain of Solofit
  6. MongoDB — The Social Layer
  7. Full Architecture Overview
  8. Key Features
  9. Challenges & How We Solved Them
  10. Business Model — Freemium Proposal
  11. Getting Started

What is Solofit?

Solofit is a real-time adaptive AI fitness coach built as a React Native mobile application. It combines clinical-grade biometric analysis, large-language-model reasoning, and a social community layer to deliver hyper-personalised fitness and nutrition plans that update every time a user's physiological state changes.

Most fitness apps ask the user how they feel. Solofit measures it.

A 10-second face scan through the phone camera captures…




Verify this in 60 seconds, no keys

  1. The whole Presage pipeline in one function. presageService.js L223-L278. Read the try block top to bottom: read file, get upload URLs, upload chunks, complete, poll. The catch at the bottom is the fallback.
  2. The readiness score. readinessEngine.js L23-L65. Start at 100, subtract. Then look at lines 62-65, which come up again in the third clock.
  3. The LLM guardrails. geminiService.js L40-L128 for the validator, and L1491-L1603 for the generate, validate, repair, fall back chain.
  4. Why the scan is 10 seconds. ScanScreen.js L36. One line, one comment. Remember it.

Onboarding welcome screen, diet plan with meal alternatives, and the XP leaderboard

Partner Technologies

Presage (the prize)

What rPPG actually is. Remote photoplethysmography. A pulse oximeter clipped to your finger shines light through your skin and watches how much comes back as blood surges with each heartbeat. rPPG does the same thing with ambient light and a camera, from a distance. The colour change is tiny and buried in noise from movement, lighting and compression, so the hard part is not the idea. The hard part is pulling a clean signal out of a shaky phone video, and that is the part Presage does for you.

What we used, exactly. The prize is called "Best Use of Presage SDK." What SoloFit ships is the Presage Physiology REST API at https://api.physiology.presagetech.com, called from React Native with plain fetch, not the on-device native SDK. The header comment in our service says the flow was "reverse-engineered from official Python client v1.6.0." In other words, Saikat read Presage's Python client, worked out the HTTP calls it makes, and wrote them again in JavaScript, because there was no drop-in React Native package for what we needed. None of the rest of us could have built this part.

The flow has five steps:

Step Call What it does
1 POST /v1/upload-url Send the file size and hr_br: { to_process: true }. Get back a job id, an upload_id, and a list of presigned S3 URLs.
2 PUT to each S3 URL Upload the video in 5 MB chunks. Keep each chunk's ETag.
3 POST /v1/complete Send the job id, upload id and the { ETag, PartNumber } list. Processing starts in the cloud.
4 POST /retrieve-data Poll every 2.5 s. 201 means still processing, 200 means results are ready. Give up after 40 tries (100 s).
5 Parse Response looks like { hr: { "0.5": 72, "1.0": 74, ... }, rr: { ... } }.

Here is the heart of it, trimmed from the source:

const { id, urls, upload_id: uploadId } = await requestUploadUrls(fileSize, apiKey);
const parts = await uploadChunks(fileBuffer, urls);
await completeUpload(id, uploadId, parts, apiKey);

for (let attempt = 0; attempt < MAX_POLL_ATTEMPTS; attempt++) {
  await new Promise((r) => setTimeout(r, POLL_INTERVAL_MS));
  const res = await fetch(`${BASE_URL}/retrieve-data`, {
    method: "POST",
    headers: { "x-api-key": apiKey, "Content-Type": "application/json" },
    body: JSON.stringify({ id }),
  });
  if (res.status === 200) return parseResults(await res.json());
  if (res.status === 201) continue; // still processing
}
throw new Error("Presage processing timed out");
Enter fullscreen mode Exit fullscreen mode

In plain English: ask Presage where to put the video, put it there in pieces, tell Presage the pieces are all in, then knock on the door every two and a half seconds until the answer is ready or 100 seconds have passed.

And the parse step, which matters more than it looks:

const hrVals = Object.values(data.hr).filter((v) => typeof v === "number" && v > 0);
hr = Math.round(hrVals.reduce((a, b) => a + b, 0) / hrVals.length);
// ...same for rr...
return {
  heart_rate: hr ?? 72,
  breathing_rate: rr ?? 15,
  stress_level: inferStress(hr),
  source: "presage_api",
};
Enter fullscreen mode Exit fullscreen mode

In plain English: take every positive sample in the time series, average them into one number, and label the result as live Presage data. Stress is not measured at all. It is inferred from heart rate alone: above 90 is high, above 72 is medium, anything else is low.

Hold on to three details from that block: every sample is averaged, nothing looks at confidence, and there is a ?? 15. All three come back later.

What Presage made easy. The server-side processing. We never touched a colour channel, a bandpass filter or a face tracker. Upload a video, get a heart-rate time series back. For a team with a 36-hour clock, that is the entire reason the feature exists.

What fought back. The upload. Presage's API has a 10 MB file limit, and a phone's front camera fills that quickly. The fix is in ScanScreen.js:

const RECORD_DURATION_S = 10; // Reduced to keep file under 10MB API limit
Enter fullscreen mode Exit fullscreen mode

Plus quality: "480p", mute: true (rPPG does not need audio), and a maxFileSize cap of 9 MB. The in-code estimate is 2-4 MB for ten seconds, which fits in a single 5 MB part. The scan had been longer (a leftover comment in the same file still says recording (15s)). We cut it to ten to make the upload fit.

That trade looked free at 2 a.m. It is the most important line in this post, and the third clock explains why.

Gemini 2.5 Flash, and the guardrails around it

The prompt is heavy. Gemini gets the full user profile, the vitals JSON, the readiness score and band, the list of injuries to work around, the equipment, the allergens (marked STRICT), and the full exercise catalog with the instruction use ONLY these, EXACT names.

A prompt is a request, though, not a guarantee. So every plan goes through validateAndNormalizeWorkoutPlan(), which throws on the first violation:

# Rule Error it throws (verbatim)
1 Exactly 7 days workout_plan must contain exactly 7 days.
2 2-4 exercises per day Day 3 must contain 2-4 exercises, got 5.
3 Only exercises from the catalog Day 2, exercise 1 uses unknown exercise: "..."
4 No duplicates within a day Day 4 has duplicate exercise "..."
5 No exercise on consecutive days Day 5 repeats "..." from the previous day. Consecutive-day repeats are not allowed.
6 At most 2 rest or recovery days workout_plan has too many recovery/rest days (3). Max allowed is 2.
7 Enough variety across the week workout_plan has low variety: 3 unique exercises, need at least 4.

Rule 7 is smarter than it looks. The variety floor is max(4, min(10, availableCount)), where availableCount is how many catalog exercises this user can actually do with their equipment and injuries. A bodyweight-only user with a bad knee cannot be asked for ten unique exercises. The comment in the code explains that without the cap, "every plan, including the deterministic fallback, [would] fail validation." Somebody found that out the hard way.

The chain around the validator:

Gemini (primary key -> backup key) -> Groq gpt-oss-120b if Gemini fails
  -> parse (with truncated-JSON recovery)
  -> validate
     -> fail: repair prompt containing the exact error -> parse -> validate
        -> fail: deterministic fallback workout -> validate
  -> tune for equipment + injuries -> validate again
  -> diet normalisation -> allergen check
Enter fullscreen mode Exit fullscreen mode

That is one generation, one targeted repair, then code. Not a retry loop that hopes for the best.

The elimination test. Could Gemini be replaced by a template? Try this user: vegan, peanut allergy, resistance bands only, knee injury, high stress this morning. A template needs a branch for every combination of those. The LLM handles the combinatorics. The validator makes sure the combinatorics came back in a shape we can trust. An LLM without guardrails is just a template with extra steps. The guardrails are the engineering.

Groq

Meal swaps only, and only for speed. llama-3.1-8b-instant, temperature: 0.2, JSON mode with a retry without it, 550 max tokens, and Gemini as the fallback. One job, done fast.

MongoDB Atlas

The social half of the System. An Express server exposes GET /leaderboard, sorted { xp: -1, streak: -1 } (XP first, streak as the tiebreak), and GET/POST /community/posts, sorted newest first, with image upload. If Atlas can't be reached, the server swaps in an in-memory collection seeded with demo users, so the screen never goes blank on stage.

Why none of this is decoration

Feature Where Why it isn't decoration
Camera vitals Presage REST API Replace it with a "how do you feel?" slider and you've rebuilt every other fitness app. The scan is the thesis.
Plan generation Gemini 2.5 Flash + Groq fallback Combinations of constraints no template covers
Plan validation validateAndNormalizeWorkoutPlan Turns a probabilistic output into a guaranteed shape
Meal swap Groq llama-3.1-8b-instant Latency budget different from plan generation
Leaderboard + community MongoDB Atlas + Express Quests without a scoreboard are just chores

One scan, all the way down

Let's go down through the layers of a single scan. This is a worked example with plausible values, not a saved reading from the event. Every arithmetic step uses the real formula from readinessEngine.js.

Layer Value Where it comes from
Video 10 s, 480p, muted, ~3 MB ScanScreen.js, in-code estimate 2-4 MB
Upload parts 1 3 MB fits in one 5 MB chunk
Job id from /v1/upload-url Presage
HR samples a time series keyed by seconds /retrieve-data, status 200
Averaged HR 88 bpm mean of every positive sample
Averaged RR 20 br/min mean of every positive sample
Stress medium inferred: 88 > 72
Source presage_api green badge
Readiness 68 decomposed below
Band light workout 45-74
Plan 7 days, lighter volume Gemini, told RECOMMENDATION: light workout
Validation passes 7/7 or repair, or fallback

Now the score by hand, for a 19-year-old with no declared health issues:

start                                             100
HR 88 > 80        excess 8, min(8 x 2, 30)        -16
HR > 0.7 x (220 - 19) = 140.7 ?   no                0
RR 20 > 18        (20 - 18) x 3                    -6
stress medium                                     -10
health issues     none                              0
                                                  ----
readiness                                          68   ->  "light workout"
Enter fullscreen mode Exit fullscreen mode

Waterfall chart: 100, minus 16 for heart rate, minus 6 for breathing, minus 10 for medium stress, ending at 68 in the light workout band

The step to watch is the breathing term. Two breaths per minute of RR moved the score by 6 points. RR is also the number the third clock is about.

Hackathon Experience

CLOCK 2 · THE BUILD · 36 HOURS

HackTropica 2K26. Asansol Engineering College, West Bengal. April 4-5, 2026. Thirty-six hours.

The theme was Nature, and how beautiful it is. We built an app that stares at your face. That sounds like the opposite of the brief until you remember the first paragraph of this post: a pulse is the oldest rhythm in nature, and it's been running under your skin your whole life. We just pointed a camera at it.

That's the event, not the product. The app demo is up in the Demo section. This is what the 36 hours looked like from outside the laptop.

The crew

We called ourselves Kolkatar Rosogollas, after Kolkata's most famous syrup-soaked sweet, and then spent a weekend building an app that writes people diet plans. I'll let that sit.

  • Saikat Das built the Presage connection: the upload, the S3 chunks, the polling, the parsing. It was the one piece nobody else on the team could have done, and the whole app stands on it.
  • Me (Subarna Maity) built the UI of the Expo app, including the redesign you're about to read about, plus the posting features and a steady stream of bug fixes as everything else broke around him.
  • Soumyadeep Dey Community posting, the onboarding flow that collects everything the prompt needs (age, goals, equipment, injuries, diet type, allergies, budget, cuisine), and the diet planning feature. So finding #6 further down is in my part of the code.
  • Sriz Debnath helped where he could, and spent much of the rest of the 36 hours on dedicated fieldwork into the food and the swag. He reports that both were excellent. Every team needs one person who remembers there's an event going on outside the terminal.

A note on reading the repo: every commit is under Saikat's account, so the log tells you when things happened, not who did them. Treat it as the team's diary, not a scorecard.

The costume came off

The biggest challenge of the weekend wasn't rPPG, or S3, or getting JSON out of an LLM. It was a question.

We had started out building the app to look like the System itself: glowing blue quest windows, notification panels, the whole anime interface, as if the phone were Jinwoo's status screen. We loved it. It was the reason the name was SoloFit.

Then the judges walked up to our table, looked at it, and asked something like: if a serious fitness person opens this app, will they stay once they see an anime theme?

We didn't have a good answer, because there isn't one. Someone who trains every morning wants their numbers, not a costume. A theme that makes four hackers grin can make the person the app is actually for close it in five seconds. That hurt more than any stack trace that weekend, because it wasn't a bug. It was taste, and the judges were right.

So we tore it down in the middle of the hackathon and rebuilt it as minimalist neo-brutalism. Every colour is now in theme.js: a warm grey #E8E8E6 background, near-black #111111 text, one loud orange #FF5A1F for anything you're meant to press, hard 1.5-2px borders, no glow anywhere. Heart rate, breathing and stress each get a single flat colour of their own. It's quiet enough for a serious lifter and blunt enough to still feel like a System.

What we kept were the bones: quests, XP, the leaderboard, "Arise." What we threw away was the paint. That rebuild is where Version 2.0.0 : Massive UI overhaul in the log below comes from.

The anime was for us. The app was for them.

The diary

Here's the diary, in IST:

Time Commit
Apr 4, 19:10 Solofit: AI-powered adaptive fitness coach app
Apr 4, 19:15 chore: Secure API keys and move Firebase config to environment variables
Apr 4, 19:18 Updated gitignore
five and a half hours of silence
Apr 5, 00:54 Presage implementation successful along with custom fallbacks
Apr 5, 01:00 feat: dynamic exercise GIFs with chevron dropdown in WorkoutScreen
Apr 5, 03:05 Harden env safety and stabilize AI workout generation
Apr 5, 03:23 Version 2.0.0 : Massive UI overhaul
Apr 5, 06:20 3.0.0 : Leader board and Community sections live using Mongo DB
Apr 5, 10:10 2.6.0 : UI changes and AI model fallbacks added
Apr 5, 12:13 version 3.0.1 : Minor changes

Commit timeline from 19:10 to 12:13 with a 5 hour 36 minute gap before the highlighted 00:54 Presage commit

Look at the gap. Between 19:18 and 00:54 there isn't a single commit. Those were Saikat's five and a half hours, and the commit that ends them is the one the whole project depends on. Our Devfolio "challenges" field sums up those hours in one line: "Implementing presage SDK into React Native App but we nailed It." The Devfolio stack also lists C++ and CMake, which are not what you reach for when calling a REST API.

The rest of the log reads like a team that has stopped being scared. Six minutes after Presage lands, someone ships exercise GIFs. By 03:23 the costume is off and the neo-brutalist UI is in. By 06:20 the leaderboard is live on Mongo. At 10:10 the version number goes backwards, from 3.0.0 to 2.6.0, in a commit that adds AI model fallbacks. That tells you exactly how much sleep was involved.

Look at the 00:54 commit message again, too. Not "Presage implementation successful." It's "Presage implementation successful along with custom fallbacks." The fallback was born in the same minute as the feature. Hold on to that.

Then judging, and the prize: MLH: Best Use of Presage SDK. We also applied to the MongoDB and Gemini tracks. We didn't win those. We won the one the whole app was built around.

[Warning] What I'd fix before anyone trains on this

CLOCK 3 · THE READING · 5 MONTHS LATER

[Warning] Vitals estimated. Intense quests should be locked. They aren't.

Five months later, sitting down to write this post, I opened Presage's current SmartSpectra docs to get one sentence right about breathing rate. This is the sentence:

Confidence is 0 until the full 30-second window is reached.

Our scan is 10 seconds long.

Go back through everything above with that in mind. The upload limit that pushed the scan down to ten seconds. The parse step that averages every sample and never checks confidence. The readiness formula where two breaths per minute cost six points. None of it was wrong at 2 a.m. It was just built without the one number that tells you whether the breathing data can be trusted at all.

These are findings, not apologies. Each one comes with its fix.

1. Breathing rate from a 10-second window. Presage's docs say breathing-rate confidence stays at 0 until 30 seconds. (Those docs describe the SDK. I haven't confirmed whether the REST endpoint behaves exactly the same.) Either way, a breathing rate from ten seconds of video is a number the provider itself says not to lean on, and our score leans on it.
Fix: On scans under 30 seconds, drop RR from the score completely. When breathing matters, scan for 30 seconds and keep the file under the limit by dropping to a lower resolution.

2. Averaging blindly. parseResults() averages every positive sample. The docs list what degrades the signal: large head, body or camera motion, chewing gum, flickering light like a TV screen. Pulse is valid for roughly 40-110 BPM.
Fix: If the API returns confidence with each sample, weight by it or throw away low-confidence samples. Either way, clamp HR to the valid range before it gets near a score.

3. A hard-coded breathing rate labelled as live data. Look at rr ?? 15 again. If Presage sends back heart rate but no usable breathing rate, the app puts in 15, and the object still says source: "presage_api". The green badge lights up for a number nobody measured.
Fix: Return breathing_rate: null and track source per field, not per object.

4. The fallback can unlock an intense workout. This is the big one. Here are lines 62-65 of readinessEngine.js:

// For estimated/mock vitals, keep readiness randomized strictly within 55-85.
if (isFallbackSource) {
  score = Math.floor(Math.random() * (85 - 55 + 1)) + 55;
}
Enter fullscreen mode Exit fullscreen mode

In plain English: if the vitals were made up, make the score up too, somewhere between 55 and 85. But 75 and above maps to "intense workout". From 55 to 85 there are 31 possible scores, and 11 of them are 75 or higher. So on a fallback scan, there's roughly a 35% chance the app prescribes an intense session from vitals that were never measured.

31 boxes numbered 55 to 85, with 75 to 85 highlighted as intense workout: 11 of 31, about 35%

I didn't have to imagine this. I ran into it while taking the screenshots for this post. Chromium's fake webcam produced no usable video file, so the app returned mock_no_video, and the random roll landed on 78:

Three real screens: Estimated Vitals with readiness 78 and Intense Workout, the dashboard saying READY, and the workout tab reading Readiness 78/100 intense workout with upper body hypertrophy on day 1

The amber badge was there, just like it's supposed to be. So were a green 78, a "READY" pill and an upper-body hypertrophy session, all built on a heartbeat nobody measured. It gets worse downstream: the vitals JSON goes into the Gemini prompt under the heading REAL-TIME BODY DATA. The source field is inside that JSON, but nothing tells the model what mock_api_fallback means.

The fallback was a good decision for a demo. The mistake was letting it go further than the badge.

Fix:

if (isFallbackSource) {
  return {
    readiness_score: null,
    recommendation: "light workout",
    details: "Vitals were estimated, not measured. Intense quests stay locked until a real scan.",
  };
}
Enter fullscreen mode Exit fullscreen mode

In plain English: when we don't know, say so, and pick the safe option. Estimated vitals can unlock a light day. They should never unlock a hard one. Tell the model the same thing in the prompt, and make the amber badge impossible to miss.

5. Two different definitions of "high stress". Real vitals call stress high above 90 bpm. The mock generator uses above 85. That's small, but it means the same heart rate gets a different stress label depending on where it came from.
Fix: One inferStress() function, used by both paths.

6. The allergen check flags instead of blocking. validateNoAllergens() does a substring match on meal names and, on a hit, renames the meal [ALLERGEN WARNING] .... It doesn't reject the plan, and it can't catch a peanut-based satay that never uses the word "peanut."
Fix: Send a flagged plan back through the same repair loop the workout validator uses, and match against ingredients, not just names.

The magic trick was real. The part we never checked was how long it needs to run.

What this unlocks

Solo Leveling was the costume, and the judges already made us take it off once. What was left underneath is one idea: measure the user before you personalise anything. Nearly every "personalised" app personalises from a profile you filled in once. A camera-based vital sign is a profile that updates every morning.

  • Study planners that check fatigue before scheduling the hard chapter.
  • Focus timers that stretch the break when your resting heart rate is up.
  • Rehab programmes that won't progress the load on a day your body says no.
  • Shift and driver apps that ask for thirty seconds before a long night, not a checkbox.

Every one of them gets the same three lessons from this post: tag the source, respect the window, and never let an estimate unlock the risky option.

FAQ

How do you measure heart rate from a phone camera in React Native?

Record a short front-camera video with expo-camera (recordAsync with quality: "480p" and mute: true), then send it to an rPPG service. With the Presage Physiology REST API: POST /v1/upload-url, PUT the chunks to the presigned S3 URLs, POST /v1/complete, then poll POST /retrieve-data until you get a 200. The response is a heart-rate time series you can summarise.

What is rPPG and how accurate is it?

Remote photoplethysmography reads the tiny colour changes in skin caused by blood flow. Presage's docs give a valid pulse range of about 40-110 BPM and list large motion, flickering light and chewing gum as things that degrade it. Its metrics are for general wellness, not FDA cleared, and not for diagnosis.

How long does a Presage scan need for breathing rate?

Presage's SmartSpectra docs say breathing-rate confidence is 0 until a full 30-second window, and HRV statistics need 60 seconds. A 10-second scan is fine for a pulse estimate. It isn't enough to trust breathing rate.

How do you stop an LLM from returning an invalid workout plan?

Don't rely on the prompt. Validate the parsed JSON against hard rules in code (for us: 7 days, 2-4 exercises per day, catalog-only names, no duplicates in a day, no consecutive-day repeats, at most 2 rest days, a variety floor). On failure, send the exact error back in a repair prompt. If the repair fails too, build the plan deterministically.

Why does my Presage poll keep returning 201?

201 from /retrieve-data means the job is still processing, not that it failed. Keep polling. We used a 2.5-second interval and gave up after 40 attempts (100 seconds), which is when Presage processing timed out fires and the fallback kicks in.

Credits

  • Team Kolkatar Rosogollas: Saikat Das (the Presage pipeline), Subarna Maity (the UI, posting and bug fixes), Soumyadeep Dey (posting, onboarding, diet planning), Sriz Debnath (morale, food reconnaissance, swag).
  • HackTropica 2K26 and Asansol Engineering College, for the 36 hours and a Nature theme we stretched as far as it would go.
  • The judges, for the question that killed our anime UI. It was the best feedback we got all weekend.
  • MLH, for the Presage track and the prize.
  • Presage, for an API that turned "read a pulse from a face" into five HTTP calls, and for docs honest enough to tell us where we went wrong.
  • Solo Leveling by Chugong, for the System we wanted to argue with.

SoloFit is a general-wellness fitness app. Its vitals are not medical measurements, and nothing in it or in this post replaces advice from a doctor. This post was drafted with AI assistance; the code, the build and the mistakes are ours.

Top comments (2)

Collapse
 
dronzer2code profile image
Subarna Maity • • Edited

@heyitsjem @jess @ben back again with another blog but this time Individual hope guys will like this one!
LaPeace

Collapse
 
c0demafia profile image
BlackDronzer •

It’s honestly fascinating to see how far technology has come. I never thought the day would come when I wouldn’t need a traditional BP machine to measure my blood pressure—now, apparently, just a simple face capture can do it! What’s even more fascinating is that Presage is offering this technology as an API service, making it possible for developers and businesses to integrate such capabilities into their own applications. The possibilities for AI-powered, convenient health monitoring are truly exciting.