
TL;DR
One-line outcome. Built an ephemeral AI skin assessment that takes a selfie, maps concerns to real treatments, and redirects to booking. No database. No auth. No stored photo.
Role, dates, scope. Product designer, architect, and solo builder. Specification authored May 2026, build in progress. Standalone tool at try.kintsumedical.com, separate repo from the main Kintsu site. Extends the Kintsu Medical Aesthetics engagement (see Kintsu Medspa).
Three design decisions worth naming.
- Ephemeral by architecture, not by policy: no database, no photo storage. The HIPAA surface is reduced to zero at the design layer.
- AI simulation feature-flagged off at launch pending attorney and image-model sign-off, so every other feature ships and works on opening day.
- Catalog filter as a first-class safety layer: the API only returns recommendations from a static vetted slug list, so GPT-4o cannot hallucinate a treatment the practice does not offer.
Context
A pre-launch medspa in a competitive market needs to convert curious visitors into booked consultations. The gap is real: a potential patient lands on the site, sees services they are unsure apply to their skin, and leaves without booking. The ageless.ai category shows the model is viable.
The constraint on this project is not technical. It is legal and clinical. An AI-generated skin analysis that presents as advice from a physician-led practice carries liability. The tool had to be useful enough to build intent, clear enough to survive a regulatory review, and honest enough that a patient with Fitzpatrick V skin understood it was designed with them in mind, not adapted from a product calibrated for Fitzpatrick I.
Mandate
Build an ephemeral AI consultation tool that captures a selfie and short intake form, runs a GPT-4o vision analysis, surfaces an aesthetic assessment and service recommendations mapped to the real Kintsu catalog, and redirects the user into the Zenoti booking flow with pre-populated intent. Store nothing. Use no login. Shut the photo out of the system the moment the analysis completes.
An AI-generated "after" simulation using gpt-image-1 image edits, watermarked via sharp, ships behind a feature flag. Attorney and image-model review clears the flag.
Scope
Solo product design and engineering. New repo, deployed to Vercel at try.kintsumedical.com. Backend is a single Vercel serverless function at /api/analyze. Frontend is React 19, Vite 7, TypeScript, and Tailwind CSS 4. No database. No auth. No third-party analytics inside the tool. Conversion tracked via the Zenoti redirect URL with ?src=ai-tool.
Wizard · Five Steps · Nothing Stored
THE FLOW · LIVE UI
The wizard in the running tool.
Landing
Value prop and ephemeral notice above the fold. Photo is never stored. This is the first thing the user reads.

Key decisions
Ephemeral pipeline · nothing persisted
Ephemeral by architecture, not by policy. The easy path was a database with a privacy policy that said "we delete photos after 24 hours." I rejected it. A policy is only as strong as its enforcement mechanism, and the breach surface is real regardless of the retention window. The harder path is architecture that makes persistence physically impossible: the image base64 travels in the POST body, passes through OpenAI, and the response returns to the browser. No write to disk. No write to a table. The api/analyze.ts endpoint is a sequential function pipeline with no persistence layer between rate-limit check and the final Response return.
Catalog filter as a first-class safety layer. The model is prompted with a strict system prompt: no diagnostic language, Fitzpatrick-aware framing, recommendations framed as "people with similar concerns often explore." But the model can still hallucinate. Both filterObservations() and filterRecommendations() run before the response leaves the server. The first guards the concern/zone vocabulary, the second drops any service_slug not present in the bundled services.json static catalog. If the model returns laser-resurfacing and the practice does not offer laser, that slug never reaches the client. The filter is tested with a unit test that asserts hallucinated slugs are dropped and the valid ones pass through unchanged.
Model Output
Server-Side Filter
Client
Feature flag for the simulation, not a deferred launch. The before/after simulation is the most engaging and most legally exposed feature. I could have blocked the full launch until legal cleared it. Instead, ENABLE_SIMULATION=false in the Vercel environment disables exactly that one path in api/analyze.ts and in ResultsStep.tsx. The consent gate, intake form, vision analysis, recommendation cards, and Zenoti redirect all ship and work on day one. The flag is a binary environment variable. Turning it on requires one change in the Vercel dashboard, no redeploy.
Turnstile bot protection without login friction. The /api/analyze endpoint needed protection against automated scraping (cost risk: OpenAI vision API calls are not cheap), but a login wall would have killed conversion. Cloudflare Turnstile solves this: an invisible challenge a real browser passes silently, a bot fails. In development without the key configured, the verification function short-circuits to true so the dev loop is never blocked. In production, a failed Turnstile challenge returns a 400 before the OpenAI client is initialized.
HIPAA-safe redirect design. Service slugs and consent metadata travel in the Zenoti redirect URL (?interest=microneedling,botox&src=ai-tool&consent=v1&consent_ts=2026-05-31T10:00:00Z). Patient identifiers do not. The buildBookingUrl() function accepts only selectedSlugs, consentVersion, and consentTimestamp. A unit test asserts no email-pattern strings can appear in the output URL. The HIPAA rule from the main Kintsu site (GA4 page_location captures the full URL on every pageview) is mirrored here at the architectural layer, not the policy layer.
Skin of color first, not as an afterthought. The system prompt names Fitzpatrick types III through VI explicitly. It instructs the model to flag PIH (post-inflammatory hyperpigmentation) risk when recommending microneedling or RF procedures for darker skin tones. The services.json catalog includes a skinOfColorNote field on each treatment entry, surfaced on the recommendation card. The patients Kintsu was founded to serve are the design target, not the edge case.
What changed
- The patient intake-to-booking funnel has a middle step that produces intent. A visitor who would have left without booking leaves with a recommendation list and a pre-populated booking link.
- The AI consultation surface handles Fitzpatrick III through VI by design, not by disclaimer. The system prompt, the service catalog notes, and the PIH risk flags encode that intent into the product behavior.
- The simulation feature is ready to activate, not a future project. One environment variable turns it on the moment the practice has attorney clearance.
Measurable outcomes
Currently in development. Outcomes to track after launch:
- Conversion rate: visitors who complete the wizard and click the Zenoti booking link.
- Booking attribution: Zenoti consultations with
?src=ai-toolin the referral string. - Step drop-off: where users abandon across consent gate, upload, processing, results, and recommendations.
MOBILE · 390px
Full wizard on phone.
- 01 / 03Landing · mobile

Serif heading wraps to two lines. Disclaimer box spans full width. CTA is thumb-reachable. - 02 / 03Intake · mobile

Age pills wrap to two rows. All ten concern chips accessible without scrolling. - 03 / 03Consent · mobile

Upload stays locked until consent is checked. The gate holds at body size, thumb-reachable.

Leadership lens
The instinct on a privacy-sensitive product is to add policy: a privacy page, a data-retention schedule, a terms update. Policy without architecture is cosmetic. I chose the architecture first, made storage impossible, then wrote the disclaimer footer. When the practice attorney reviews this tool, the most persuasive thing in the packet is not the privacy policy. It is the fact that there is no database to breach.
The feature flag is the same logic applied to the simulation risk. Scope the legally exposed feature into a flag. Ship everything else on time. The risky path launches when the gate clears, not when the whole product launches. This is not risk deferral; it is risk isolation.
What I did with my hands
Player-coach proof, all authored personally:
- The full implementation plan (13 tasks, TDD structure, unit tests written before each module):
docs/superpowers/plans/2026-05-30-kintsu-ai-consultation-tool.md. - The system prompt in
api/lib/prompts.ts: clinical framing, Fitzpatrick-aware instructions, JSON schema contract for structured output, inline slug allowlist enforcement, PIH risk rules for Fitzpatrick IV through VI. - The ephemerality contract in
api/analyze.ts: no side effects between the rate-limit check and the finalResponsereturn. Reviewable as a linear 60-line pipeline. - The service catalog in
src/data/services.json: 28 face services with concern mappings, descriptions, andskinOfColorNotefields authored from the Kintsu treatment menu. Body, IV, and weight-loss services were intentionally excluded. This is a face tool. - The Topix take-home product catalog in
src/data/products.json: 17 curated retail products mapped per concern, selected server-side (never model-generated, which keeps product recommendations out of the attorney-gate risk surface). - Upload and webcam capture, unified through a shared encode helper, with a fake ring light (the camera screen in full-white
bg-whitemode acts as a fill light, the dominant accuracy lever for the vision model). - The redirect URL builder in
src/lib/redirect.ts, with a unit test asserting that no email-pattern string can appear in the output URL. - The watermark module in
api/lib/watermark.ts: SVG overlay composited onto the simulation image viasharp, labeled "AI Simulation. Not a real result. Individual results vary."
AI threading
The product is an AI feature, not an AI-assisted build. Gemini 2.5 Pro runs the vision analysis, currently active, A/B'd against gpt-4.1 via the feedback loop. Both providers are wired through the Vercel AI SDK using generateObject with a Zod schema, so the structured output contract is validated at the SDK layer rather than hand-parsed. Provider and model are runtime-selected via a VISION_MODEL env var (openai:gpt-4.1 | google:gemini-2.5-pro). Gemini was chosen because it showed stronger spatial and skin-tone discrimination on the concern types that matter most for Fitzpatrick III to VI skin.
Images are sent at detail: 'high'. An earlier detail: 'low' path was a regression. It collapsed every photo to one 512px pass and discarded the tone, texture, and pigment detail the tool exists to read. Fixing it required tracking down a non-obvious provider compatibility constraint: Gemini's API requires images as a raw Buffer, not a data:image/...;base64, data URL. The base64 prefix causes a silent decode failure. The system prompt also had to move from a role:'system' message into the SDK's dedicated system parameter, which Gemini silently ignores when passed as a message. Neither issue surfaced until real integration testing.
The system prompt is the design artifact of this work. It is not a prompt that asks the model to "analyze skin" and returns free-form text. It contracts a specific JSON schema (assessment.summary, assessment.observations[].concern, assessment.observations[].note, assessment.fitzpatrick_estimate, recommendations[].service_slug, recommendations[].why, recommendations[].skin_of_color_note), forbids diagnostic language by name, lists the slug allowlist inline, and instructs the model to default conservative on Fitzpatrick estimation when skin tone is ambiguous in the photo. A model that follows the contract returns structured, usable data. A model that breaks the contract has its hallucinated slugs dropped by filterObservations() / filterRecommendations() before the client sees the response. Two separate guards, one for concern/zone vocabulary, one for service slugs.
The simulation path (feature-flagged off) uses gpt-image-1 image edits: the uploaded photo as the base, a conservative prompt capped at three concerns, then sharp watermarking before the base64 payload leaves the server.
The build itself was implemented with Claude Code using a task-by-task TDD plan: unit tests written before implementation, an integration test with mocked OpenAI before the endpoint was assembled, and Playwright e2e covering the consent gate, the ephemerality invariant (photo cleared on step advance), and the Zenoti redirect.
Reflection
The hardest constraint to hold was not the HIPAA rule or the legal exposure on the simulation. It was the Fitzpatrick concern. Skin analysis AI that treats Fitzpatrick I as the default and applies the same confidence to Fitzpatrick V is not a neutral choice. It is a product decision that excludes the patient demographic Kintsu was founded to serve. The system prompt, the skinOfColorNote fields, and the PIH risk flags are not compliance checkboxes. They are the design intent of the product.
Running this again I would get the attorney into the review loop before the system prompt was finalized, not after. The feature flag approach is correct but it is expensive if the flag stays off for months while the simulation prompt is revised to satisfy a late legal review. Shorter loop: draft the prompt, review it with counsel and the Medical Director together, ship the prompt and the flag in one pass.




