A digital layer for physical consignment and second-hand stores. Every one-of-one garment gets a QR code. Shoppers scan it to virtually try on that exact piece, build a virtual rack, and decide which items deserve a real trip to the fitting room.
Try the rack, not the fitting room.
Virtual try-on is usually an e-commerce feature. Physical second-hand retail has an unusually strong need for it:
- every garment is a one-of-one — no restock in another size;
- one rack contains French sizing, vintage Italian, US sizing, altered cuts, and decades of conventions;
- shoppers have to physically try many pieces just to decide what is worth considering;
- fitting rooms become part of discovery rather than the final confirmation step.
The goal is not to replace the fitting room — it is to turn ten fitting-room experiments into three high-confidence candidates.
Shopkeeper (intake): photograph the garment → run the YouCam readiness pipeline → enter inventory and consignment details → generate a QR tag and attach it to the physical item.
Shopper (scan): upload one reusable photo → scan any QR in the store → see that exact garment on themselves via YouCam Clothes V3 → add promising pieces to a virtual rack → shortlist for the real fitting room.
- Node.js 20+
- A YouCam API key (Clothes V3 required; Shoes optional)
- An OpenRouter API key (for AI garment cleanup and item-analysis autofill)
git clone <repo>
cd youcam-ai-hackathon
npm installCopy .env.example to .env.local and fill in your keys:
cp .env.example .env.local| Variable | Required | Description |
|---|---|---|
YOUCAM_API_KEY |
Yes | YouCam API key — server-side only, never exposed to the browser |
YOUCAM_API_BASE_URL |
Yes | YouCam API base URL (default: https://yce-api-01.makeupar.com) |
NEXT_PUBLIC_APP_URL |
Yes | Public URL of this app (used for QR code generation) |
OPENROUTER_API_KEY |
Yes | OpenRouter API key — server-side only |
OPENROUTER_NANOBANANA_MODEL |
Yes | Model for garment photo cleanup (default: google/gemini-2.5-flash-imagerem) |
OPENROUTER_VISION_MODEL |
Yes | Model for item-detail autofill (default: google/gemini-3-flash-preview) |
npm run dev # development server at http://localhost:3000
npm run build # production build
npm start # serve production buildnpm test # run full test suite (Vitest, no watch)
npm run test:watch # watch mode
npm run lint # ESLint| Layer | Technology |
|---|---|
| Framework | Next.js 16 (App Router, Turbopack) |
| Language | TypeScript 5, React 19 |
| Styling | CSS Modules |
| State | React useReducer + context (no external state library) |
| Persistence | IndexedDB via idb (locally created items only; session state only for shopper photo) |
| QR generation | qrcode.react |
| QR scanning | @zxing/browser |
| Testing | Vitest + Testing Library + fake-indexeddb |
| Deployment | Vercel (serverless functions for all API routes) |
All API keys live only on the server. The browser never sees YOUCAM_API_KEY or OPENROUTER_API_KEY.
Browser
│
├── POST /api/youcam/clothes/tasks → YouCam task creation
├── GET /api/youcam/clothes/tasks/[taskId] → YouCam task poll
├── POST /api/youcam/shoes/tasks → YouCam Shoes task
├── GET /api/youcam/shoes/tasks/[taskId] → YouCam Shoes poll
├── POST /api/openrouter/garment-cleanup → Nano Banana photo cleanup
└── POST /api/openrouter/item-analysis → Vision model item autofill
YouCam's VTO is asynchronous. The app models the full state machine:
idle → preparing → submitted → processing → success
↘ error (recoverable / terminal)
For Try the Rack, each item has its own independent task state so one failure does not destroy the rack session.
src/
├── app/
│ ├── api/
│ │ ├── openrouter/
│ │ │ ├── garment-cleanup/ # POST: Nano Banana photo enhancement
│ │ │ └── item-analysis/ # POST: vision-model garment attribute autofill
│ │ └── youcam/
│ │ ├── clothes/tasks/ # POST + GET [taskId]: Clothes V3
│ │ └── shoes/tasks/ # POST + GET [taskId]: Shoes VTO
│ ├── items/[id]/ # Shopper item page (seeded + locally created)
│ ├── shopkeeper/
│ │ ├── intake/ # Multi-step consignment intake wizard
│ │ └── tags/[id]/ # QR tag preview + print
│ └── shopper/
│ ├── photo/ # Shopper photo upload
│ ├── rack/ # Virtual rack + results
│ └── fitting-room/ # Fitting-room shortlist
│
├── components/
│ ├── qr/ # QR generator + printable tag
│ ├── shopkeeper/ # Inventory card, status badges
│ ├── shopper/ # Item hero, rack item card
│ └── vto/ # VTO result display
│
├── data/ # Seeded demo garment fixtures
│
├── features/
│ ├── intake/ # Intake reducer, wizard, steps, photo enhance
│ ├── rack/ # Rack reducer + results
│ ├── shopper/ # Shopper session + IndexedDB persistence
│ └── vto/ # Single-item VTO orchestration
│
├── lib/
│ ├── db/ # IndexedDB (idb) shopper state store
│ ├── env.ts # Server-side env validation helpers
│ ├── fit/ # Measurement fit estimation
│ ├── image/ # Canvas rotation utility
│ ├── items/ # Item resolver (seeded + local)
│ ├── openrouter/ # garment-cleanup.ts, item-analysis.ts
│ ├── qr/ # QR ID generation
│ ├── vto/ # YouCam task client
│ └── youcam/ # YouCam API wrappers + category routing
│
└── types/ # Shared TypeScript types (ConsignmentItem, etc.)
src/lib/openrouter/garment-cleanup.ts — calls OpenRouter with a fidelity-preserving prompt (Nano Banana model). Accepts ArrayBuffer, returns a base64 data URL. Never logs the API key or raw payloads.
src/lib/openrouter/item-analysis.ts — vision-model adapter that returns structured ItemAnalysis with per-field { value, confidence, source }. Drops malformed suggestions silently.
src/features/intake/item-analysis-policy.ts — confidence policy gate. brand and labelSize require confidence ≥ 0.90 and source visible_text. name/category/color require ≥ 0.80. material requires ≥ 0.85. Never overwrites a non-empty field.
src/lib/image/rotate-image-data-url.ts — browser-only canvas utility. Draws rotated pixels into a new canvas and returns a data URL. Used by PhotoEnhancePanel so the exact rotated pixels flow into YouCam validation and IndexedDB.
src/features/intake/reducer.ts — single useReducer for the full intake draft. Handles photo lifecycle, AI analysis state, VTO readiness, and item patch with field-overwrite protection.
src/lib/db/shopper-store.ts — IndexedDB store (secondlook-demo / shopperState) for locally created items. Shopper photos are never persisted — they live only in React session state.
| Method | Route | Body | Response |
|---|---|---|---|
POST |
/api/youcam/clothes/tasks |
{ shopperImageUrl, referenceImageUrl, category } |
{ taskId } |
GET |
/api/youcam/clothes/tasks/[taskId] |
— | { status, resultUrl? } |
| Method | Route | Body | Response |
|---|---|---|---|
POST |
/api/youcam/shoes/tasks |
{ shopperImageUrl, referenceImageUrl } |
{ taskId } |
GET |
/api/youcam/shoes/tasks/[taskId] |
— | { status, resultUrl? } |
| Method | Route | Body | Response |
|---|---|---|---|
POST |
/api/openrouter/garment-cleanup |
multipart/form-data with image file |
{ dataUrl } |
Accepted MIME types: image/jpeg, image/png, image/heic, image/heif. Max 10 MB.
| Method | Route | Body | Response |
|---|---|---|---|
POST |
/api/openrouter/item-analysis |
multipart/form-data with image file |
{ analysis: ItemAnalysis } |
No production database. Seeded fixture items live in src/data/. Locally created items are stored in IndexedDB for the current browser only.
type ConsignmentItem = {
id: string;
qrId: string;
brand: string;
name: string;
category: string;
labelSize: string;
sizeSystem?: string;
color?: string;
material?: string;
condition?: string;
fitNotes?: string;
measurements?: { chest?; waist?; hips?; length?; inseam?; other? };
originalImage?: string; // original merchant photo — never analyzed after this point
vtoReferenceImage: string; // YouCam-ready reference (may be enhanced + rotated)
vtoProvider: "youcam-clothes-v3" | "youcam-shoes";
garmentCategory?: "upper_body" | "lower_body" | "full_body";
vtoReadiness: "unchecked" | "checking" | "ready" | "retake";
consignorNumber?: string;
sellingPrice: number;
currency: string;
consignorShare?: number;
storeShare?: number;
intakeDate?: string;
expiryDate?: string;
};YOUCAM_API_KEYandOPENROUTER_API_KEYare never exposed to the browser, passed inNEXT_PUBLIC_*variables, logged, or included in thrown errors.- Customer photos are not persisted to any database, server filesystem, analytics service, or localStorage. The
Filelives only in React state for the current session. - Shopper measurements are not sent to YouCam or OpenRouter.
- Item analysis output is never used to modify the VTO reference image.
- The AI autofill policy never overwrites a field the shopkeeper has already filled in.
The app ships a small catalogue of seeded garments for cross-device QR demos (a consistent QR resolves without a database). Routes are slugs like /items/max-mara-wool-coat. Each fixture has static metadata and a bundled reference image.
The intake wizard still works fully for new items — they are saved to IndexedDB and accessible on the same browser session.
- Persistent inventory backend
- Consignor profiles and commission ledger
- Shoes VTO polished flow
- Background VTO pre-processing at intake
- AI measurement guidance and size-system normalization
- Online catalogue / marketplace export from intake data