Inspiration
Week three of a new serum is when the doubt starts. You stand at the bathroom mirror, your chin looks worse than when you started, and you have to decide: is it failing, is it a purge that will pass, or is today's light just harsher? You scroll back through your gallery and every photo was taken at another angle, under another lamp, at another distance. Comparing selfies by eye cannot answer the question, and a chatbot that has never seen your skin cannot either.
So people decide on a feeling. They drop a salicylic acid serum on day 20 when it needed eight weeks, or they keep buying a niacinamide toner that never moved anything. The challenge brief named exactly this moment: "right when they're standing in front of a mirror deciding whether a product is working". I built Kept for it.
What it does
Kept treats every new product as a trial on your own face.
- Add the product. Pick its actives from a list of 17 or paste the INCI list from the box. The actives decide the goals (a salicylic serum: acne, pores, oiliness), the areas watched for irritation (redness, plus hydration for retinoids), the trial length (56 days for salicylic acid, 84 for retinoids) and the check-in days.
- Take the day 0 photo. A camera guide checks face size, head angle, light and sharpness in the browser before anything is uploaded.
- See your target. YouCam AI Skin Simulation shows the same face with the goal areas improved, and Kept analyses that simulated face to get a number to aim for on the last day.
- Check in. Every check-in shows a faint outline of your day 0 face so you stand where you stood before. YouCam AI Skin Analysis scores each area and returns its skin maps.
- Get a verdict. Too early to tell, On track, Working, Likely purging, Your skin is reacting or Not doing enough, always with two to four reasons in numbers, for example Acne is up on day 18. Retinal often causes a short purge in weeks 2 to 6. Blemishes 16 → 34 on the acne map. Then a next step: keep going, use it every other night, keep it or stop it, with a calendar file for the next check-in.
Around the trial:
- Before you buy: Paste the ingredients of something you're about to order and Kept checks them against your own history. If a niacinamide toner did nothing for your pores in 56 days, it tells you before you buy the next niacinamide toner.
- Sunscreen: SPF won't show anything in eight weeks, so Kept doesn't pretend it will. Instead it shows your face over the years with YouCam AI Aging Simulation, which is a much better argument for wearing it every day.
- Trial report: One printable page with day 0, the latest photo, the target, the chart and a score table, for a dermatologist or a shop.
- Ask about this trial: A small assistant that answers questions like "why is my acne worse?" using the trial's own numbers. It explains the verdict and never changes it.
For shops and brands the use is pretty direct. If a store offers Kept after a purchase, fewer people give up on a working product in week two, and a "results or your money back" promise can be settled with a report made from photos taken the same way every time.
How we built it
Stack. Next.js 16 (App Router) with React 19 and strict TypeScript, deployed on Vercel. There is no database and no account: trials, photos, skin maps and scores live in the browser's IndexedDB. The server is a thin set of route handlers that hold the API keys, check spending limits and talk to YouCam and OpenRouter.
I use four YouCam APIs, each for one job:
| Step | API | Endpoint |
|---|---|---|
| Every photo | File API | POST /s2s/v2.0/file, then PUT to the presigned URL |
| Baseline and each check-in | AI Skin Analysis v2.0, SD | POST /s2s/v2.0/task/skin-analysis and polling GET …/skin-analysis/{task_id} |
| The trial's target | AI Skin Simulation | POST /s2s/v2.0/task/skin-simulation |
| Sunscreen | AI Aging Simulation | POST /s2s/v2.0/task/aging |
Kept only asks for the areas a trial needs, usually four, which keeps a check-in at 9 units. I also used text-to-image/youcam image generator once in a script to make the sample face, so the demo doesn't use a real person.
Capture. MediaPipe Face Landmarker runs in the browser on every camera frame: face width between 55% and 80% of the frame, yaw and pitch within 10°, roll within 8°, brightness of the cheeks and forehead in range, Laplacian sharpness, and at check-ins the mean distance of 12 landmarks from the day 0 photo under 0.1 interpupillary distances. A photo that fails never reaches YouCam.
Skin maps. YouCam returns a mask PNG per area. A Web Worker counts blemishes on the acne mask (connected components of alpha above 40, at least 12 px) and measures the red area on the redness mask. The verdict quotes both, because a score can stay flat while the map changes.
The verdict engine is deterministic TypeScript. Each of the 17 actives has a first change day, a full effect day, an optional purge window and an irritation level (retinal: 21, 84, days 14 to 42, high). The rules run in order: reacting, purging, too early, working, not enough, on track. The expected score follows an S curve from day 0 to the simulated target:
$$\text{expected}(d) = \text{base} + (\text{target} - \text{base}) \cdot s(d)$$
where $s$ is 0 until half the onset day and 1 on the full effect day.
Noise calibration. A change only counts if it beats the difference between repeat photos. I analysed one face five times: the original, shifted 3% and scaled 0.97, 6% brighter, 6% darker and rotated 1.5°, and blurred 0.6 px. Acne scored 66 on all five. Redness ranged 63 to 64, pores 66 to 70, texture 73 to 77. The largest two-sigma spread was 3.03 points, so
$$\text{NOISE} = \lceil \max 2\sigma \rceil = 4$$
and every reason in the app says so: Pores 69 → 80. That's more than the ±4 points we see between repeat photos.
The assistant runs Gemini 3.8 Flash through OpenRouter with streaming and four tools (get_scores, compare_days, active_timeline, expected_at) that call the same domain functions as the engine. It gets up to four tool rounds, about 90 words for the answer, and photos only if the person switches them on.
The sample shelf has three trials built from real YouCam responses: a salicylic serum that worked (acne 66 → 74, pores 69 → 80, blemishes 49 → 11), a niacinamide toner that did nothing in 56 days, and a retinal cream on day 21 in its purge window.
Tests. Vitest covers all six verdicts, purge against reaction, plans, the INCI parser, Before you buy on the sample shelf, blemish counting on real YouCam masks, normalisation of a recorded YouCam response, the quota and the assistant's tools and agent loop.
Challenges we ran into
Purge and irritation look the same in the numbers. In both cases the acne score drops in the first weeks of a retinoid. I separated them by timing and by a guard area. A drop in acne inside the active's purge window, with redness holding, is a purge; a drop in redness or hydration beyond 4 points outside that window, or beyond 8 points at any time, is a reaction. Building the demo showed us how close this is: on the retinal trial, a day 7 frame simulated at strength 0.6 dropped redness from 80 to 75. That is beyond the noise and outside the purge window, so the engine called it a reaction, as the rule says it should. I rebuilt the frame instead of loosening the rule.
Eight weeks of demo data in two days. Nobody can wait 56 days for a hackathon. I generated one face, then used Skin Simulation on it at strengths 0.25, 0.5, 0.75 and 1.0 as days 14, 28, 42 and 56, gave each frame a small shift and brightness change like a real repeat photo, and analysed every frame with Skin Analysis. The niacinamide toner's check-ins are simply the five calibration photos, so its "Not doing enough" is honest. Face, calibration and all three trials took 30 YouCam tasks and 222 units.
The salicylic simulation moved the map, not the score. At the intensities I first planned (acne 0.6), the acne score stayed at 66 while the number of rings on the acne map fell. That is why Kept counts blemishes on the mask and reports both.
Protecting the balance on a public demo. YouCam charges for engine errors too. My first sample face candidate failed with error_src_face_too_small (the face must cover more than 60% of the image width) and still cost 12 units. So the camera guide rejects bad photos before upload, the app never retries a task on its own, and the server caps spending per instance and per IP and keeps a reserve on the account.
Masks that expire and refuse the browser. Result links live for 2 hours and YouCam's S3 buckets send no CORS headers, so the browser cannot read a mask directly. A server proxy streams them and the app stores them in IndexedDB right away. My first version accepted any amazonaws.com host whose bucket name started with yce-, and in review I realised anyone can create such a bucket and make my domain serve their file. The proxy now accepts only YouCam's three exact result hosts, only image types, and sends a sandboxing content security policy.
Accomplishments that we're proud of
- A verdict that can say wait. Before an active's first change day (day 28 for niacinamide) Kept answers Too early to tell and explains why, and on day 18 of a retinal cream it tells a purge from a reaction.
- The target from Skin Simulation is a number, not only a picture. Analysing the simulated face gave the trial a destination, and on the salicylic sample the measured acne on day 56 (74) landed right next to the simulated target (72).
- The ±4 threshold printed next to every reason was measured with YouCam, and the code, the method page and the report all use the same value.
- No photo leaves the browser except to be analysed, and the whole app works without an account.
What we learned
YouCam's scores are deterministic: the same photo gives exactly the same numbers. That moved my attention from the model to the photo. Almost all of the variation comes from how the picture is taken, which is why so much of Kept ended up being the camera guide and the day 0 outline.
I also learned that the simulation is local. In my first probe I simulated acne and redness, and redness jumped from 62 to 82 while pores, which I didn't touch, stayed at 70. That's what made the target curve possible.
What's next for Kept
- Measure the noise on real repeat photos from volunteers in their own bathrooms, per area instead of one shared threshold.
- Timelines that take concentration into account (2% salicylic acid behaves differently from 0.5%) and more actives.
- An embeddable version a shop can start from the order page, with the report as proof for results guarantees.
- Sync between devices and a link that shares one trial with a dermatologist.
Built With
- ai-aging-simulation
- ai-skin-analysis
- ai-skin-simulation
- css
- gemini
- indexeddb
- mediapipe
- motion
- next.js
- node.js
- openrouter
- perfect-corp
- react
- server-sent-events
- sharp
- typescript
- vercel
- vitest
- web-workers
- webassembly
- youcam-api
Log in or sign up for Devpost to join the conversation.