Skin Cipher
A mobile skincare app that scores a selfie with Google Gemini, backed by a Go API I wrote, where most of the engineering effort went into making repeated AI scores come out consistent.
Overview
Skin Cipher is a skincare tracking app. The user takes a selfie and gets a 0–100 skin score with five metrics (Hydration, Elasticity, Texture, Tone Evenness, Pore Health), a short summary, concerns and recommendations. Around that core scan the app has an onboarding questionnaire that builds a skin profile, morning and evening routines, AI product, barcode and ingredient analysis, progress tracking, community challenges and a way to ask a dermatologist a question. I built both the Expo mobile app and the Go REST API, including the Gemini integration.
The business idea
Skincare advice is scattered and generic. People buy products without knowing whether the ingredients suit their skin, and they have no way to see whether a routine is working. Skin Cipher gives users an AI skin rating from a selfie, builds a skin profile through onboarding, and personalizes every product and ingredient check to that profile. It also tracks routines, progress and community challenges, and lets users ask dermatologists questions.
Goals
- Give people a quick, repeatable way to track how their skin changes over time from a selfie
- Make scores stable enough that a change in the number means something, not model noise
- Personalise product advice using the user's onboarding skin profile
- Keep users coming back with routines, daily streaks and community challenges
Tech stack
Key features
- Take a front-camera selfie and get a scored skin analysis with five metrics, concerns and recommendations
- Answer a 20-step onboarding questionnaire (skin type, sensitivity, concerns, lifestyle) that builds a skin profile used in the AI product prompts
- Search a skincare product by name and get a compatibility score, key ingredients, pros and cons for your skin profile
- Compare two products side by side and see which one suits you better
- Paste an ingredient list, or photograph the ingredient label, and get a per-ingredient safety breakdown
- Scan a product barcode to identify it and analyse it
- Photograph a shelf of products and get a compatibility score for each one it recognises
- Build morning and evening routines with editable steps, daily completion tracking and a voice-guided walkthrough
- Track progress with daily score history, metric trends, calendar and graph views, and editable weekly goals
- See a monthly analysis that compares the previous and current month
- Join community challenges, check in daily, and view leaderboards, rank and suggested challenges
- Keep a day streak that advances once per calendar day, with push notifications at streak milestones
- Ask a dermatologist a question, with a one-free-question-per-week quota
- Check local weather and UV index for the day
- Sign up with email verification, sign in, and reset a forgotten password
- Talk to support through a live chat screen inside the app
- Submit products to the community catalogue
Challenges & solutions
The same photo could score differently on repeat scans (for example 78, then 75), and the same product could score 90 once and 95 the next time. Temperature 0 alone didn't fix it.
For skin scans I send temperature 0, a fixed seed and a thinking budget of 0. Product, ingredient and barcode analysis use temperature 0 with the same fixed seed. The prompts also carry explicit scoring rubrics with numeric anchors, and tell the model to use a neutral 75 for any metric it can't clearly see.
The model's overall score drifted independently of the five metrics it reported, and an earlier version fell back to random mock data when Gemini failed, which produced inconsistent ratings.
The backend now computes the overall score as the rounded average of the five metric values instead of trusting the model's own score field. If Gemini fails or returns unparseable JSON, the skin scan returns an error rather than saving a made-up result.
Gemini returns 429 rate-limit errors under load, and 2.5-flash is a thinking model that mixes reasoning parts and markdown code fences into its response.
A shared Gemini caller retries rate-limit, 5xx and network errors up to 3 times with growing backoff and a 60-second timeout. It picks the last non-thought text part and strips code fences before parsing the JSON.
The day streak went up by one on every scan, so a user who scanned three times on their first day saw 'Day 3'.
CreateScan now compares the date of the previous scan with today's date. A second scan on the same day leaves the streak alone, the next consecutive day adds one, and a gap of more than a day resets it to 1. A test file for the scan service covers this logic.
Lessons learned
- Don't let a model report a number that should be derived from other numbers. Computing the overall score from the five metrics in AnalyzeImage removed one source of drift that prompt wording couldn't fix.
- For repeatable output, temperature 0 isn't enough. The code comments record that without a fixed seed and a zero thinking budget, identical input still drifted by a few points.
- Fail visibly, not with a plausible fake. The skin scan used to fall back to random mock ratings when Gemini failed, which made scores inconsistent between scans. It now returns an error so the user can retry. I haven't yet applied the same fix to the other AI features.
Known limitations & next steps
- Product analysis, comparison, ingredient analysis, barcode analysis and shelf analysis still return mock data when Gemini fails or returns invalid JSON, and users can't tell those results from real ones. Next step: apply the skin-scan pattern (return an error, show a retry) to all AI features.
- The app has no automated tests. The 13 unit tests cover the backend only.
Screenshots




Want something like this built?
Tell me about your project and I'll get back to you.