The moment

You are standing in front of a mirror with a product in your cart. It says it fixes your pores. Nobody has measured your pores, and the only advice on offer is yes.

The problem

A recommendation that can't say no isn't advice. The skin-analysis score behind many recommenders also has a flattering twin: YouCam's docs say the display score (ui_score) is "adjusted to produce more favorable results". On my three sample faces, grading on that score instead of the raw score changes 8 of 36 verdicts, and it produces no Match at all.

What Receipts does

Receipts is a trust layer for AI skin advice. YouCam is the sensor. Receipts decides, and says when it can't.

  1. Scan. One photo goes to YouCam Skin Analysis: 15 raw concern scores from 1 to 100, plus a detection mask for each. Upload a photo or use your camera.
  2. Decide. Plain arithmetic, no language model, gives each product a Match, Partial or Skip: the weakest concern the product targets, against two cut-offs. The receipt shows the score and the mask on your face as proof.
  3. Refuse when unsure. If the deciding score is inside the measured photo-noise margin of a cut-off, the verdict is Too close to call. One extra photo can be averaged in to try to resolve it.
  4. Check your own cart. Paste the products you are about to buy, with prices, or paste a product's whole ingredient list. Each gets a verdict for your face, and you see what skipping keeps. It matches label claims and ingredient names (and shows where each active sits on the label). It says nothing about whether a product works.
  5. Preview, then audit the preview. Skin Simulation shows a predicted result, saved for the sample faces and made live on your own scan. For the saved previews I send the image back through Skin Analysis and print whether YouCam's own analyzer measures the change.
  6. Share it. A receipt image with the verdicts and total, with no face on it.
  7. Where a retailer would use it. A demo store page with a "Checked for your skin" chip beside Add to Cart.

Why this is not a thin wrapper

I did not stop at displaying YouCam's numbers. I measured how far to trust them, and that is the part of the project that is mine:

  • The same photo scans identically (within 0.01), but a different photo of the same face does not. A 25% darker copy moved pores by 10.6 points.
  • A stability study. Ordinary photo changes (darker, warm light, tilt) flipped 7 of 180 verdicts across 5 faces, and the "too close" band flagged 4 of them in advance.
  • A real holdout. The margins come from one face and were fixed before I scanned the others. On the 4 faces they were not fitted to, 6 verdicts flipped in 12 photo changes. The band flagged 3. All 6 came from the darker photo. So the band is conservative but not complete, and lighting is the main risk.
  • Five more faces, darker photo only. Different skin tones, ages and light. The darker photo flipped 4 of 60 verdicts and the band flagged 3 of those 4. Reported on their own; no figure above uses them.
  • YouCam's own previews, re-measured. Of 5 saved Skin Simulation previews, 3 show a change its analyzer can measure (eye bags +17.7, dark circles +8.9, oiliness +18.4). Both pore previews show none (+1.7 and -1.4).
  • The sensor-swap test. Nothing in the rule or the noise test is specific to YouCam except the input format. What I built is the audit method, not a YouCam screen. (The cut-offs and margins are calibrated to YouCam's scale and would need re-fitting for another sensor; I have not run another vendor.)

I measure repeatability, not accuracy. I have no dermatologist labels, and I say so.

How I built it

Decision Who decides
What the skin looks like (scores, masks, preview images) YouCam Skin Analysis and Skin Simulation
Match, Partial, Skip, Too close My code (public/verdict.js), no language model
Whether a score is inside the photo-noise margin Margins measured by scripts/margins.py and committed
Whether the verdicts are right A separate Python script, written from the README rules, that re-derives all 36 verdicts and the margins from the raw scans and must agree
Buying anything You

The site is plain HTML, CSS and JavaScript with no build step, on Vercel. Two serverless functions (api/scan.js, api/simulate.js) hold the API key, check the account balance before any live call and pause below a floor. A rate-limited call is retried once and nothing else is retried, so a scan can never be charged twice. Sample faces and previews are saved in the repo, so judging never spends units. Every page loads in with a top-down blur-fade, and respects reduced motion.

Mapping to the Skin AI brief

The brief says What Receipts does
"People wonder about their skin right before a purchase" You paste your cart or an ingredient list and get a verdict per product before you pay
"know exactly what to do next" Skip, buy, or rescan: every receipt ends in one of those
"not surface-level wrappers" The audit: stability across more than 10 faces, a holdout, and YouCam's previews re-measured by YouCam
"functional web prototype that delivers clear consumer value" Live at the link above, sample faces need no key, a real scan takes about 10 seconds

How YouCam's APIs are used

  • Skin Analysis (v2, SD): 15 concerns plus skin type and skin age, 16 units a scan, raw scores and masks. Used for every verdict, every stability scan, and to re-measure previews.
  • Skin Simulation (v2): 4 units a preview, for the before/after slider, saved for sample faces and live on your own scan.
  • Both are called only from my serverless functions. The browser never sees the key.

What changed during the hackathon

The first commit is Fri Oct 30, 2026, 21:57. Everything in this repository was built after that, in this hackathon: the verdict rule and its independent re-check (Oct 30), the three-page site and the receipt overlay (Oct 31), the stability study, evidence page, "too close" state, cart, share card and demo store (Oct 31 to Nov 1), and on Nov 1 the camera selfie, ingredient-list check, live Skin Simulation previews, the shared API client with a capacity check, five more test faces, CI and the load-in animation.

Evaluation

  • 48 automated tests, including boundary tests at every cut-off and margin, and mutation checks (I broke the rules on purpose and confirmed the tests go red). They run in CI on every push.
  • npm run recheck: an independent Python re-derivation, 36 of 36 verdicts agree, and the shipped margins equal the margins it derives from the raw scans.
  • Contrast checks for both themes, keyboard use, reduced motion, and no horizontal scroll from 320 to 1440 px.

Challenges

  • A stale stylesheet made the site look broken. My local Python server sent no cache headers, so the browser reused an old CSS file with new HTML. I wrote a small no-store dev server and a test for it.
  • My first deploy exposed photos I meant to keep private. The Vercel CLI ignores .gitignore, so a local-only folder went live. I added a .vercelignore, deleted the deployment, and now probe the leak paths after every deploy.
  • Live scans lost their masks. The API returns maskUrl; my page read mask. I mapped it and made the test mock use the real shape.
  • Hydration reads 52 to 62 on every face, which would hand everyone a hydration Match. It got its own cut-offs.
  • Some test portraits were rejected (error_src_face_too_small, error_src_face_out_of_bound). Failed scans are free; I cropped until the face fit, and one over-large face needed headroom.
  • A scan failed once on a transient error and worked unchanged on the rerun. I could not see why, so the live endpoints now retry a rate limit once and log the status and error code of any failure.
  • The pore preview did not move the pore score. I kept it in the evidence instead of hiding it.
  • A live preview was stricter than a live scan. Skin Simulation refused a face that Skin Analysis had scanned fine ("keep your whole face inside the frame"), so the page shows that message instead of hiding the button.

Honest limits

  • Three sample faces and seven test portraits. Read the counts, not the percentages.
  • The noise margins come from one face, and the pore margin (plus or minus 14.3) is driven by it. The band flagged only some of the flips on faces it was not fitted to.
  • The test portraits are mostly dramatically lit photos, a harder case than a selfie by a window.
  • Live previews are predicted, not re-measured (re-measuring costs another scan).
  • The shelf is fictional, and shelf prices are made-up demo prices. Cut-offs are my own heuristic.
  • Cosmetic guidance only, not medical advice.

What's next

A larger and more varied set of faces for the margins, tested under real home lighting; a way for a retailer to feed in its own catalog; real before-and-after photos over weeks.

Built With

Share this project:

Updates

Submission history