Inspiration
I bought a bohemian midi skirt and could not figure out a top to go with it.
Not because the skirt was bad. Because I had bought it alone, the way you buy everything online, without knowing what it was going to live next to. Every top I owned belonged to the person I dressed like a year ago. The skirt belonged to the person I was trying to become.
That is how it goes every time I try to shift my style. I buy one piece from an aesthetic I am moving toward, and my existing wardrobe cannot meet it. The clothes I never wear again are always the same ones: the pieces I cannot pair with anything. They are not mistakes. They are orphans.
And when I shop, I do something slightly absurd to compensate. I scroll customer review photos hunting for a reviewer with roughly my measurements who looks roughly like me, so I can guess how something will actually sit. I am assembling a body double by hand, out of strangers, because no store will show me the garment on me.
So the question I actually wanted answered was never "does this top look good." It was: will this create an outfit I will really wear, with the piece I already own and refuse to give up?
Nothing I could find answers that.
What it does
Keeper is a second opinion on an outfit that already failed.
You photograph an outfit you wore and hated. You mark the one piece you are keeping, the piece you refuse to give up. Then a handful of taps about it: how much room it has, where it sits, why you are keeping it, what the weather is doing, what you are dressing for, and what went wrong. One question is required. The rest are optional, and each one either changes a rule or is the warm question on the screen.
Keeper returns three complete outfits that keep your piece and replace everything else, rendered on your own body, from garments that are in stock right now.
The keeper never changes. Same colour, same drape, same length, across all three results.
It works in both directions. Keep a bottom and it searches tops. Keep a top and it searches bottoms.
Before any of that, it reads your face once. The palette that comes back is not a theme. It is computed from your actual skin, hair and eye colour, and the entire interface repaints itself in it. Every garment you are shown from that point is filtered through your colouring.
Everything you save goes behind the piece it was built around. Your keepers become gates. Nothing enters your wardrobe except through something you already love.
How I built it
The catalog is live, not seeded. Every Shopify storefront exposes its full catalog as JSON at
/products.json with no key and no login. That gave me 1,805 real garments and 2,354 colourways from
Edikted, with per-variant stock availability, so nothing Keeper recommends is a colour the store does
not actually have. Colourways with no size in stock are dropped at ingest, not filtered at display.
Colours are read from pixels, not names. Retail colour names are marketing copy. "Sage," "mocha" and "bone" mean different things at different brands and nothing to a computer. Every variant image is processed to extract its dominant garment colour and converted to CIELAB for comparison.
The colour engine takes the hex values from YouCam's AI Facial Color Tones Analyzer and derives three things:
- Undertone, from the hue angle across the a* and b* axes in Lab space
- Depth, from L*
- Contrast, from the lightness gap between hair and skin
Contrast is the one almost nobody implements, and it is the reason the same colour can look striking on one person and costumey on another. High contrast between hair and skin means saturated colours hold up and pastels disappear. Low contrast means muted, tonal palettes flatter and brights overwhelm. Undertone alone cannot tell you that.
The styling rules are a rules engine, not a model. Opposing volume, waist definition, formality
proximity, colour harmony. A voluminous bottom wants a fitted or structured top. Something in the
outfit has to define a waist. These are pattern-cutting principles, and they are legible and
debuggable in a way a black box is not. Aesthetic is scored on top of them from Edikted's own tags,
because formality measures how dressed up something is and says nothing about which world it belongs
to. A crochet skirt and a graphic tee are both casual, and only one of them is the answer.
Rendering uses YouCam's AI Clothes API with garment_category always set explicitly, never
auto. Every render is cached to disk keyed by a hash of its inputs, so no pairing is ever paid for
twice.
286 tests, concentrated on the colour engine and the styling rules, which is where correctness actually lives. A wrong render is obvious. A subtly wrong palette is not.
A fixed budget of 1,000 API units shaped the architecture. Everything runs behind a mock layer by default, every response is cached by input hash, and capability was validated on free tools before anything was paid for. The recorded demo runs on 18 units of cached renders and spends nothing to perform.
Challenges I ran into
Finding the right endpoint in the suite. I started out assuming Skin Analysis was where colour lived. It is a genuinely deep API, but everything it returns is dermatological: acne, pores, redness, wrinkles, texture. Excellent for a skincare product, not what I needed to decide what someone should wear.
The right tool was the AI Facial Color Tones Analyzer, and it turned out to be better than what I had been looking for. I expected a warm/cool/neutral label. It returns skin and eye colour as raw hex, which meant I could compute undertone, depth and contrast myself instead of accepting someone else's classification. The whole colour engine exists because that endpoint hands back numbers rather than a verdict.
I also learned to verify each capability empirically rather than reasoning about it. Testing on the free consumer tool first, then confirming with a single paid call, resolved my riskiest assumptions in the first two days and cost almost nothing. Flat-lay product images working for lower-body try-on was one of those, and it is the reason keeping a top and searching for bottoms is possible at all.
Contrast, twice. My colour engine derives contrast from the lightness gap between hair and skin. It is the dimension that explains why the same colour can look striking on one person and washed out on another, and nothing else in the palette calculation can substitute for it. It is also the only input that depends on a value the API turned out not to measure.
The first failure was a constant. hair_color came back as #FAF0BE for every face I sent, which is
the named web colour "Blond." On a fair subject nobody would notice. On a deep complexion, a pale
blond value against dark skin inflates the gap, so contrast read high when the truth was low and
the palette came back inverted. The placeholder does not blur the answer, it flips it. Hair is now
measured directly from the image region, so the gap is always computed from two independent samples.
The second failure was worse, and it arrived on the fourth face I tested. A professional headshot
returned hair_color: #B56637, a coppery mid-brown. Her hair is near-black. It was not a placeholder
and it was not on my list of known-bad values, so the guard I had built to catch wrong values trusted
it absolutely and handed back a palette byte-identical to a completely different person's. Measured
directly, the hair-to-skin gap went from 1.1 to 37.0 and the palette changed.
A placeholder is easy to catch. A plausible lie is not. That is the version of this problem I would not have found by reasoning, and I only found it because I kept testing on faces that did not resemble the sample image. For a product built entirely on complexion, that turned out to be the most important hour I spent.
Accomplishments I am proud of
The keeper survives. That sounds small until you watch three renders in a row and the skirt is identical in all of them.
The three results are different from each other and all three are defensible. This is the part I did not expect to be hard. A recommender that plays it safe returns three versions of the same top and tells you nothing, because you cannot choose between things you cannot distinguish. One that chases variety returns something arbitrary in the third slot and you stop trusting the other two.
Against the brown midi skirt, Keeper returns a fitted bralette in brown, a halter in dark brown, and a boat neck in olive. Three different necklines, three different colours, and every one of them earns its place: each is a fitted shape against a loose bottom, and each colour sits inside the palette computed from her face. You can pick a favourite without any of the others looking like filler.
That came out of the rules engine being explicit rather than learned. Coverage filters first, because a hoodie beside a summer skirt is not a bold suggestion, it is a wrong one. Then opposing volume, waist definition, formality proximity and colour harmony constrain what is left hard enough that anything passing is genuinely wearable, while diversity is still free to vary anything inside that set.
It works in both directions, which means it is a general tool rather than a top-recommender with extra steps.
The contrast dimension of the colour engine says something specific and non-obvious about why a colour suits someone, and it is computed from their actual face rather than a quiz.
And it is honest when it has nothing to add. Ask again without changing anything and it returns what you already have, and tells you which answer to change to get something different, rather than spending your money to generate the same three outfits twice.
What I learned
Test the free consumer tools before writing a line of code. Three of my riskiest assumptions were resolved for nothing in the first two days, and each one would have been expensive to discover later.
Verify behaviour rather than reasoning about it. Every assumption I checked with a single paid call was worth two units many times over.
Look across the whole API suite before committing. The endpoint named for the domain is not always the one holding the data you need, and in my case the less obvious one gave me something better to work with.
Write guards against the shape of the error, not a list of known bad values. A blocklist only catches the failures you have already seen.
And test on faces unlike the sample. Both contrast bugs were invisible on the face I developed against, and both would have shipped.
What's next
Worlds. A garment is not orphaned universally. It is orphaned in a context. A skirt can be dead for the office and perfect on a weekend. Keeper is built around bohemian and streetwear because that is where the catalog is deepest and where the real use case lives. Business is 22 tops and 1 bottom in this catalog, so it needs a different retailer rather than a different rule.
Demand signal for retailers. Keeper already knows which cuts pair successfully and where people abandon at the colour step. A merchant seeing "shoppers loved this silhouette and left because the palette was wrong" is information no analytics tool currently provides.
More catalogs. The Shopify ingest generalises to any store on the platform with no per-retailer work.
Buy for the gap. The natural extension of the gate model: point Keeper at a product page and ask not whether the garment is nice, but how many complete outfits it creates from pieces you already own.
Built With
- next.js
- react
- sqlite
- tailwind
- typescript
- vitest
Log in or sign up for Devpost to join the conversation.