← projects

Uru

Jun – Dec 2025 · ended

Technical co-founder · ~500 commits · iOS, two backends, infrastructure

A mobile-first AI cross-listing tool for fashion resellers, supporting Poshmark, Grailed, and Mercari. Co-founded with Gemma Sun.

The app, and the browser workflow it initiates — the rare early footage that survived.

The problem

The idea came from a friend who listed the same clothes on several marketplaces—including Poshmark and Depop—because it helped them sell faster. Every listing had to be recreated by hand, and after an item sold, the duplicates had to be removed.

We were also seeing a large community of younger fashion resellers describing similar workflows on TikTok. That was a market signal, not yet customer validation. Existing cross-listing software was primarily built for professional or high-volume sellers working in batches from a laptop. We believed there was an underserved segment of younger, casual fashion sellers who managed their listings from their phones.

Our solution

A mobile app where a seller takes a photo once, reviews an AI-generated preview shaped to each marketplace’s catalog, and publishes from the phone. The model proposed the copy, categories, sizes, and colors; the seller remained in control before anything went live.

By the end of the project, the working product scope covered Poshmark, Grailed, and Mercari. The complete publish-and-delete lifecycle was built and directly tested for Poshmark; I do not attach its timing or test coverage to the other two platforms.

preview, generated by an AI model One listing written once Review each per-platform preview Publish tracked per platform Sold on one delisted from the rest Poshmark Mercari Grailed category, size, colors

What I owned

My co-founder and I made the major product decisions together. I owned the technical side end to end: the native iOS app, AI listing generation, backend services, infrastructure, and the browser automation that carried a reviewed listing into each marketplace.

Signing in as the seller

None of these platforms provides a public API. Publishing means driving the site the way a person would: sign in as the seller, walk the listing form, and upload the images. Every platform has its own login flow and bot detection.

Existing solutions sidestep that. They are browser extensions, so they run inside the seller’s own browser, which is already signed in to every site they use.

A phone has no such browser to borrow. The seller signed in to each marketplace through a browser view inside the app, and a backend worker restored that user-authorized session in a headless browser to run the workflow. It made the mobile experience possible, but it also introduced difficult reliability, security, and platform-policy constraints.

a browser extension the seller's own browser already signed in automation nothing has to move Uru the phone signs in through a browser view in the app a worker on the server restores the session in a headless browser the credential has to the cached credential

Making the preview instant

Every platform has its own listing requirements and catalog, and publishing creates an external side effect. The seller therefore has to see the listing for each platform, as that platform will show it, before any of it goes out.

That means holding a copy of every platform’s catalog ourselves—a data pipeline collects it—because when the model writes the description and fills the fields, it has to choose from the options that platform actually offers. Many of those choices are conditional: men’s fashion, then outerwear, then leather jacket. Each step has to wait for the one before it, and the seller waits for all of them. The first version took about 15 seconds.

Running the independent choices in parallel, and moving to Gemini Flash, brought that down to 1–3 seconds.

before after ≈15 s 1–3 s each choice waited for the one before it dashed: the choices that depend on nothing, moved alongside the chain

Durable workflow execution

Publishing one listing is 30-plus seconds of browser automation against a site that can rate-limit, challenge, or simply change its form. That cannot live inside a request: the app would hold a connection open with nothing to show, and a retry could publish the item twice.

So a publish request only enqueues. A control plane records the job and puts it on a queue, a worker picks it up and runs the automation, and the worker emits an event when it lands. Anything that fails past its retries lands in a dead-letter queue, where it can be inspected and replayed rather than lost.

iOS client Swift Business API NestJS · Prisma Workflow API FastAPI · control plane SQS queue + dead-letter queue Worker Python · Playwright Poshmark · Mercari · Grailed Postgres listings, publish status DynamoDB job execution state completion events

A retry replays the form from the preview the seller already approved, so it costs a browser session and not the reasoning again.

Browser automation

How a listing actually gets onto a site changed four times in six months.

  1. The private API. Poshmark’s own site talks to a REST API, so we called it directly—create a draft, fill it, upload the images, publish. Fast and exact, and their bot detection caught it. The calls were not coming from a real browser.

  2. Replayable action lists. An agent would work through a page in a ReAct loop and leave behind a list of actions that could be replayed later without the model. It needed an expression evaluator, a dependency resolver, and an interpreter.

  3. Choosing selectors manually. ReAct-style LLM browser automation was not reliable enough at the time, so we went with the most predictable thing available: ordinary web selectors. That meant opening each page, reading its HTML, and picking the selector for every field manually. The actions were generated from the preview the seller had already approved—navigate, fill the basic fields, select the brand, size, and colors, upload the images, and publish, with every value settled before the browser opened.

  4. webtask. What a model is good at is finding one element. Not planning a flow—just: here is a button, described in a sentence, point at it. So I wrote a library that does only that—webtask, on PyPI. Mercari and Grailed were built on it, and adding a platform stopped meaning a fresh set of selectors to write and maintain.

Why we stopped

Because it was our first startup, we tried to build a fairly complete MVP before putting it in front of users. Reliable cross-listing was harder than we expected: every marketplace had a different login flow, bot detection, catalog, and changing website. Supporting several at once consumed months while I was also working full time at Amazon.

We built substantial parts of the product, but we did not get a version reliable enough into real users’ hands. Without a customer feedback loop, the team eventually lost momentum and we decided not to continue.

What I learned

I brought big-company instincts into a two-person startup: 13 repositories, three services on ECS with CDK and per-service security groups, and abstractions for another marketplace before the first one had a user. In a startup without validated demand, the architecture should optimize for learning speed and reversibility, not for organizational boundaries the company may eventually need.

webtask became a useful technical artifact and a public record of what I learned about browser automation. But for Uru, I extracted it too early. It should have remained an internal component until real product usage and a second use case justified the abstraction. The lesson was about sequencing, not that reusable infrastructure has no value.

Today I would get one marketplace working end to end, put it in one seller’s hands, and learn from real usage before adding another platform. The problem was not necessarily impossible; we tried to solve too much of it before validating the smallest useful version.