Inspiration

Secondhand is the fastest growing part of fashion and the only honest answer anyone has to 92 million tonnes of textiles being thrown out every year. It also does not scale, and the reason is embarrassingly mundane: somebody has to sit down and make the listing.

Clothes don't sit in the closet because people don't want to sell them. They sit there because listing them is miserable.

Doing one listing properly means working out what the piece actually is, digging through comparable listings to find a real price, finding somewhere with decent light to photograph it, writing a description, and then doing all of that again for the next item. It's 15 to 30 minutes each. A full closet is an afternoon nobody has, so the clothes stay where they are and eventually go in a bag.

We looked at where the time actually goes and found two separate walls.

The first is pricing. Knowing what a used garment is worth means searching what comparable pieces actually go for. Not guessing, and definitely not asking a language model to guess for you.

The second wall is photography. A proper product shoot runs $500 to $2,000. That is exactly why individual sellers post dim bedroom photos and then accept dim bedroom offers.

Plenty of people have attacked the pricing wall. Nobody has attacked the photography wall. That gap is where Rack lives, and it only became buildable this year, when the fashion try-on APIs that make it possible actually shipped.

What it does

You lay several garments out, take one photograph, and upload it. Then Rack does six things.

0. Splits the photo into garments. Perfect Corp's background removal returns the frame as an RGBA image whose alpha channel is a mask of everything that is not background. Two jumpers on a bed are two separate islands of opaque pixels in that mask, so finding them is connected-component labelling over a bitmap: no second vendor, no model, no extra key. Each region is cropped out of the original photo and becomes an ordinary item, which means the pieces then run through the rest of the pipeline in parallel with each other. One photo of two garments produces two finished listings in about forty seconds, which is what a single garment costs. The second piece is nearly free, and that is the entire difference between a listing tool and emptying a closet.

The limit is honest and measured: garments that physically touch are one shape in the mask and come back as one item. Where they are joined only narrowly, eroding the mask boundary breaks the bridge and recovers both pieces, and that erosion is attempted only when plain labelling already found nothing to split, so it can add detections but never break a correct single-garment answer. Where two garments genuinely overlap, no amount of eroding separates them without also shattering a shirt at its waist, and we would rather return one honest item than two invented ones. In practice the gap needed is small: 80 pixels of daylight in a 2500-pixel photo, about 3% of the frame, splits cleanly. Both behaviours are covered by tests so they stay known rather than discovered.

1. Identifies each piece. SerpApi's Google Lens engine gives us the brand and the garment type. Lens only returns a knowledge graph for catalogued products, so secondhand clothing is misidentified often enough that the brand is editable in place: click it, type the real one, and pricing and the listing copy re-run from the correction. New comps, new median, new title.

2. Prices it. Three more SerpApi engines. Google Shopping gives the retail anchor, so you know what it cost new. eBay gives live comparable listings, which is the number that actually matters. Google Trends gives twelve months of demand direction, so you know whether to list now or hold. The suggested price is the median of those listings with the 25th to 75th percentile range around it, and every single comp is on the page as a clickable link to the listing it came from.

3. Photographs it. Perfect Corp's pipeline. Background removal lifts the garment off the bedspread, enhancement sharpens it, then clothes try-on renders it worn on a synthetic model. No studio, no lightbox, no photographer, no model.

4. Writes the listing. Title and description generated from the attributes we identified.

5. Publishes it. A claim-your-storefront step searches name.com for names that are actually available, and one click runs the sequence: availability check, registration, the A record, the www subdomain, and URL forwarding to the shop. Search, availability and registration complete against name.com's sandbox; the three DNS calls return 404 there, because the sandbox registers a domain without provisioning a zone behind it, and the panel says so per step rather than showing six green ticks it cannot back. Same code path, same requests, and they land in production.

The domain is not just a receipt. A hostname resolves to its store, Caddy issues a certificate on demand through a gated endpoint that only answers for domains a store actually registered, and the shop is served at that address. A seller who already owns a domain can point it here without any registrar call at all.

Stripe attaches a checkout to each piece, showing the on-model render and the description, and collects the buyer's shipping address at payment.

Every piece adds to a running inventory total, computed from what the store has actually published. One photograph of several garments takes about forty seconds end to end.

How we built it

Spring Boot 4 (Java), React, PostgreSQL, Docker, AWS EC2.

One rule governs the entire system: no number a user sees is ever generated by a language model. The price is the median of real comparable listings, computed arithmetically, with the sources rendered right next to it. AI writes listing copy and normalises brand strings. That is the whole division, and it's enforced in the code. PriceCalculator contains no model call and it never will.

The interesting engineering is in the orchestration. A batch fans out to 30 or 40 asynchronous calls across four vendors, so this isn't a wrapper around one API.

  • Every external call is a row in a Postgres tasks table with a status, an attempt count, and an idempotency key, so a retry can never double charge vendor credits.
  • A scheduled worker dispatches independent item chains in parallel across a bounded pool, with an in-flight guard so a tick never re-dispatches work that's still running, and a four attempt ceiling so one bad photo can't loop forever.
  • Perfect Corp polling uses exponential backoff starting at 500ms and capped at 2 seconds, with a hard 120 second ceiling per stage.
  • Every imaging stage fails soft and is independently feature flagged. If the try-on fails on one item, the previous image is kept, the other items still publish, and the batch settles to PARTIAL_FAILURE instead of hanging.
  • An item with no comps gets failed instead of listed, and it doesn't get photographed either, because spending vendor credits on something we can't honestly price is just waste.

40 tests, none of which need network access. They pin the exact JSON shape each vendor returns, which turned out to be where the real bugs actually live.

Challenges we ran into

The bug that would have shipped. SerpApi's Google Shopping engine returns a flat extracted_price. Its eBay engine nests the same value under price.extracted. Our parser read the flat field for both. Every eBay row got silently discarded, the comp list was always empty, the median was always null, and every publish threw. The pipeline was dead end to end and every test passed the whole time, because the fixture had been written to match our code instead of captured from the actual API. That lesson was expensive and very specific. A fixture you wrote yourself only proves your code agrees with itself.

Google Lens doesn't describe what you photograph, and our first two fixes were both wrong. Lens returns a knowledge_graph only for catalogued products. Secondhand clothing on an unmade bed is not one, so what comes back is visual_matches and nothing else, which gave us a blank brand, an empty eBay query, and no comps.

Our fallback picked the word appearing most often across the match titles, on the theory that the word recurring in independent listings of the same garment is the brand. A real pair of jeans came back branded "Solid". So we added the colour and fabric words to a blocklist, re-ran the same photo, and got "Long". That is when it became clear the approach was wrong rather than incomplete: frequency genuinely cannot distinguish an adjective from a brand, because descriptors recur across independent listings in exactly the way brands do, and every blocked word just promotes the next one.

What works is a different kind of signal. A list of apparel brands is checked first, longest name first so "American Eagle" is not truncated to "American", and a name has to appear in two independent listings to count. Only when nothing is recognised does it fall back to counting, now weighted by position, because marketplace titles are written brand-first. The same photo now reads Zara Jeans, and the comps improved with it, because the eBay query is a real brand instead of an adjective. No model call either way, so it stays reproducible live on camera. No fixed list covers secondhand clothing, which is exactly why the brand is editable in place and the correction re-runs pricing and copy.

Latency that only shows up at batch size. Perfect Corp polling sat on a fixed 15 second floor with a five minute ceiling, dispatched serially. Four imaging stages at the time, across six items, worked out to two hours. It compiled, it passed everything, and it would have destroyed the demo. Any sleep inside a loop has to be multiplied out by the real batch size before you accept it.

A data source disappearing three days out. The original design priced from eBay sold listings, which is the strongest possible number. Then we found show_only=Sold returning zero rows for every query and taking 27 seconds to do it, while an unfiltered search returned 60 rows in under two. eBay moved sold and completed listings behind a login in July 2026. So we had a choice: keep the stronger sounding claim, or change the product to match what we could actually prove. We relabelled every surface, the UI, the generated copy and the docs, from "sold" to "listings". The entire premise is that you can click the number and check it yourself, and a claim you can't verify is worth less than a weaker one you can.

An API that wasn't what it looked like. ai-studio read like a product photography backdrop generator. When we queried its live template endpoint we got back female_pink_bunny, male_neon_bokeh and female_disco_dust. Those are themed portrait scenes for staging a person, not backdrops for a flat garment. We cut the stage rather than ship something that would occasionally render a jacket into a disco.

A fix that only half worked, kept anyway. Splitting a photo into garments fails when two pieces touch: they are one shape in the mask. We tried eroding the mask boundary to break the join, which works on the narrow case where a sleeve rests on a hem. On genuinely overlapping garments it does nothing, because the contact area is far wider than any erosion that does not also shatter a shirt at its waist. We kept the erosion, guarded so it only runs when plain labelling already found nothing to split - it can add detections, never break a correct single-garment answer - and we measured the real boundary instead of claiming the problem solved: 80 pixels of gap in a 2500 pixel photo separates cleanly, zero gap does not. Both behaviours are covered by tests so they stay known rather than discovered.

A transaction holding a connection through a vendor call. Garment detection is a network round trip of several seconds, and it was running inside the transaction that writes the batch, so every upload held a database connection for its whole duration and concurrent uploads were capped at the pool size for work that touches no database. The obvious fix - extract the write into another method on the same class - is a trap: Spring's @Transactional is proxy-based, so a method calling an annotated method on this bypasses the proxy and silently runs with no transaction at all. That failure is invisible and worse than the problem. The write went into its own bean instead, with the store resolved by id inside the transaction rather than handed in as a detached entity.

Knowing where to stop. We cut cross-posting to eBay and Depop entirely, because the OAuth review cycles alone would have eaten the whole timeline. We also cut every sponsor integration that wasn't doing real work. Four vendors doing something load-bearing beats seven doing decoration.

Accomplishments that we're proud of

A price you can audit. Click any comp on any listing and you land on the actual eBay listing it came from. When there are fewer than four comps the page says so instead of projecting confidence it hasn't earned. When there are none, the item doesn't list at all. In a category full of confident AI valuations, ours is arithmetic over evidence you can check.

We inverted virtual try-on. Every other use of these APIs is buyer side: see it on you before you buy. We pointed the whole suite at the seller instead, who has no model, no studio, and a phone camera in bad light. As far as we can find, no listing tool on the market produces a photograph. They all still make you shoot it yourself.

It degrades instead of breaking. No Perfect Corp key and the original photo becomes the catalog image, pipeline continues. No name.com credentials and the storefront serves at a path. No Stripe and the page says contact the seller. Every one of those is a tested path, not a hope.

What we learned

The dangerous bugs live in the code nobody reviewed. We reviewed the logic we wrote by hand and found real problems in it, an off by one in the median and some missing null guards. But the defect that would have sunk the whole project was in generated integration code that was internally coherent, compiled cleanly, and was simply wrong about somebody else's JSON. No amount of rereading it would have caught that. Only opening the vendor docs and diffing the field names did.

We also learned that shipping something honest is a design constraint with real teeth. Refusing to let a model produce a price forced actual decisions. What do you do with two comps? With zero comps? With a brand you guessed wrong? Every one of those answers made the product better than a confident number would have.

Why this could be a company

The judging asks whether this becomes a startup, so here is the actual arithmetic rather than a hand wave.

What one listing costs us. Six vendor API calls and about twenty seconds of compute: four SerpApi searches to identify and price the piece, two Perfect Corp calls to sharpen it and render it on a model. Storage is a handful of images. That is the entire marginal cost of a listing.

What one listing costs a person today. Fifteen to thirty minutes, or roughly $500 to $2,000 if they pay a photographer to do the part Rack automates. The gap between those two numbers is the business.

Who pays, and how. Three routes, in the order we would actually try them. A per listing fee, which is the simplest thing to test and the easiest to explain. A take rate on items that sell through the storefront, which aligns us with the seller rather than charging them up front. And licensing the imaging pipeline on its own to marketplaces whose sellers upload bad photos, which is all of them. That third one is the interesting business: pricing is a commodity, the photograph is not.

The honest part. Margin is not the hard problem here. Distribution is. Sellers already live on eBay, Depop and Poshmark, and a tool that asks them to move is a tool they will not adopt. That is why cross posting is the first thing on our roadmap and not a nice to have: Rack should prepare the listing and let people sell where they already have an audience. We would rather be the layer underneath those marketplaces than a competitor to them.

What would tell us we are right. Every listing is a labelled data point: this price, this photo, sold or didn't, in this many days. If sell through on Rack photographs beats sell through on the seller's own photographs, the whole thesis is proven in numbers rather than argued. That is the first experiment we would run with real users.

What's next for Rack

Export to where the buyers already are. A CSV for eBay bulk upload and a copy listing button for Depop and Poshmark, so Rack prepares the listing and you sell wherever you already have an audience. Then OAuth cross-posting with auto-delist.

Garments that touch. Splitting a photo into pieces works when they are laid out with daylight between them, and 80 pixels of gap in a 2500 pixel photo is enough. Two garments physically overlapping are one shape in the mask, and eroding the boundary far enough to separate them also shatters a shirt at its waist. Proper instance segmentation is the fix, and it is the difference between "lay them out" and "throw them on the bed".

Sell-through feedback. Every listing is a labelled data point: this price, this photo, sold or didn't, in this many days. That's the flywheel, pricing that improves from outcomes instead of comps alone.

Photography as a service. The imaging pipeline is the piece no competitor has. It stands on its own as an API for any marketplace whose sellers upload bad photos, which is all of them.

Built With

Share this project:

Updates

Submission history