-
-
Architecture: a Next.js app with tested domain services, connected to YouCam AI Skin Analysis, Groq or Gemini, and Postgres
-
SkinProof learns how much your skin scores wobble on their own, then only reports changes bigger than that noise.
-
Scan result with 15 concerns, skin age and a YouCam redness map. Here, every change stays within noise, so none is called progress.
-
A 28-day, single-product trial. Redness rose +8.4 against a ±3.7 noise band: "Clear change, 2.3× your normal wobble" (demo data).
-
Drag a slider across two scans and see each concern against your noise band. Only 2 of 15 changes count as real (demo data).
-
Before you buy: paste an ingredient list and get a verdict based on your scan and shelf.
-
The routine planner orders your products and moves clashing actives to alternate nights, with a Sunday recovery night: 1 clash → 0.
-
Breakout triage: scan the flare-up, log sleep, stress, diet and new products, then let the coach rank likely triggers (demo data).
-
Verified Results for brands: anonymized, measured outcomes, shown only after 3+ consenting reviewers (fictional brands, demo data).
-
A shareable proof card with the measured result of a finished trial, and no photo or name (fictional brand, demo data).
-
Today dashboard: skin score trend, check-in streak, trial progress, tonight's routine and skin weather (demo data).
Inspiration
You spent $60 on a serum. Six weeks later, you're standing at the mirror wondering if it did anything.
Skincare is a huge purchase category with almost no feedback loop. We buy on promises, judge by memory and mirror, and blame or credit whichever product we used last. AI skin analysis looks like the fix, because it gives you a number. But point any scanner at the same face twice and the number moves with the light, the angle and the distance. An app that announces "+3 redness!" may just be reporting a cloud passing the window.
That became the question behind SkinProof: how do you tell a real change in your skin from noise in the measurement? The hackathon brief named three moments (right before a purchase, right after a bad breakout, and in front of a mirror deciding whether a product is working), and that question runs through all of them.
What it does
SkinProof is a measurement-first skincare co-pilot built on YouCam AI Skin Analysis:
- Calibration: two photos a minute apart show how much your scores wobble on their own.
- 28-day trials: one product, weekly scans, and a verdict of working, no change or worsening, judged against your personal noise band.
- Before you buy: a label photo, product link or pasted ingredient list gets a buy, buy with care or skip answer, based on your weakest measured concerns and what's already on your shelf.
- Breakout triage: a breakout scan and a 20-second diary let the coach rank likely triggers and suggest a gentle plan for tonight.
- Clash-free routines: an ordered AM/PM routine that moves clashing night actives onto alternate evenings.
- Verified Results: reviews that carry the reviewer's measured change, aggregated for brands with a minimum group size of three to protect anonymity.
How I built it
The noise model is the core. Two scans of the same face minutes apart differ only by noise. From $n$ calibration pairs with differences $d_i$, the per-measurement noise is estimated as:
$$\hat{\sigma} = \sqrt{\frac{\sum_{i=1}^{n} d_i^2}{2n}}$$
A change between two future scans has standard deviation $\sqrt{2}\,\hat{\sigma}$, so the 95% band for each concern, with a floor of 2 points, is:
$$T = \max\left(2,\; 1.96\sqrt{\frac{\sum_{i=1}^{n} d_i^2}{n}}\right)$$
With a single pair, that reduces to $1.96\,|d|$. A change $\Delta$ only counts as real when $|\Delta| > T$, and the app reports it in plain language using the ratio $r = |\Delta| / T$, as in "Clear change, 2.4× your normal wobble."
The stack:
- Measurement: YouCam's S2S flow (upload, create a task, poll for results) with all 16 analysis actions. Scores, skin age and zone masks are saved, and masks are copied before YouCam's 24-hour retention ends.
- App: Next.js 16, React 19, Tailwind v4, Drizzle ORM, and PostgreSQL with an embedded PGlite fallback, so the project runs from a fresh clone with no database setup.
- Coach: a tool-calling agent with 9 user-scoped tools, running on the free Groq or Gemini tier. It is told never to claim a change inside the noise band.
- Ingredients: the LLM only reads labels and pages. Mapping ingredients to actives and concerns, and every clash rule, comes from plain, tested tables. The routine planner uses graph colouring over those same rules, so it can't disagree with the clash warnings.
- Testing: a mock YouCam client kept development from using up the 1,000-unit budget, and 62 unit tests cover the logic where a silent bug would mislead users.
I also added a Method & evidence page where anyone can test the claim against known answers: change nothing (scores should stay in band), apply concealer (should be detected), or flush your skin (should be detected). These scans are quarantined, so a concealer photo can never count as progress.
Challenges
- One calibration pair is a noisy estimate of noise. When I seeded demo data with random calibration noise, the same underlying wobble produced bands anywhere from ±2 to ±16. That's the statistics of estimating spread from a single difference. The formula stayed, but the app now pools every calibration pair, invites users to repeat calibration, and applies a floor.
- A bug the demo data hid. Testing with a fresh user showed that people weren't calibrated after taking both photos. Scan status only advanced for the scan being viewed, so the first photo stayed "running" forever. The seeded demo wrote scores directly and never hit this path. The app now settles in-flight scans before any comparison.
- The live API differed from what I'd assumed. My first real scan failed to save because YouCam returned several
skin_typeitems for one task, some with null scores, and my parser expected one per type. The save rolled back cleanly and the result was still available to re-read, so no units were lost. The parser now merges repeated items. - Free-tier LLMs are unpredictable. On my first live run the coach timed out because the default Gemini model was returning 503s, and other models soon returned 429s. Each call now falls through a chain of models and skips busy ones quickly.
- You can't wait 28 days during a hackathon. I built a developer-only time-travel tool that shifts one user's records into the past rather than faking the clock, since "now" is read in too many places to offset safely. It's off by default and refused for the demo account.
- Fetching user-supplied links safely. Reading product pages meant guarding the server against SSRF. My first blocklist rule also blocked every public site, because Node matches IPv4 addresses against an IPv4-mapped IPv6 range. Tests now check allowed addresses as well as rejected ones.
What I learned
- Honest uncertainty can be a feature. "Within noise" sounds like a disappointing result, but it's what makes "clear change" believable.
- Let the LLM read and let rules decide. Keeping safety-relevant logic in deterministic tables made it explainable and testable, and it can't invent a clash.
- Test as a brand-new user with real data. Mocks and seeded demos are useful, but my worst bugs lived exactly where they didn't reach.
- A measurement is only valid under its conditions. Lighting is such a big source of noise that the camera now has a live light meter and warns you when the light differs from your calibration photos.
What's next
- Shrink single-pair noise estimates toward a population prior, so a user's first band is more stable.
- Validate the noise bands across skin tones, a limit the app already states openly on its Method & evidence page.
- Pilot Verified Results and the "will this work for me?" widget with brands and retailers.
Built With
- drizzle-orm
- gemini-api
- groq
- next.js
- node.js
- open-meteo-forecast
- openai-sdk
- pglite
- postgresql
- react
- recharts
- sharp
- tailwind-css
- typescript
- vitest
- youcam-api
- zod