{"slug": "building-an-ai-clothes-changer-provider-abstraction-async-jobs-and-a-credit-that", "title": "Building an AI Clothes Changer: provider abstraction, async jobs, and a credit system that won't lose money", "summary": "A developer launched Dressora, an AI clothes changer for virtual try-on, and detailed the backend architecture behind it. The system uses a provider abstraction to swap AI services easily, async job handling with webhook callbacks and re-upload to R2 storage, and a credit system with freeze/settle/release logic to prevent double-charging. The project is live at aiclotheschanger.me.", "body_md": "I recently launched [Dressora](https://aiclotheschanger.me/), an AI clothes changer that swaps outfits onto a single photo for virtual try-on. The product side is fun, but the parts I actually sweated over were the boring backend bits: orchestrating multiple AI providers, handling long-running generation jobs, and building a credit system that never double-charges or loses money. Here's what I learned.\n\nAI providers change pricing, rate limits, and quality constantly. Hardcoding one is a trap. I put everything behind a small factory:\n\n``` js\nconst provider = getProvider(\"evolink\");\nconst task = await provider.createTask({ prompt, aspectRatio });\n```\n\nEach provider implements the same interface (`createTask`\n\n, `handleCallback`\n\n, status mapping). Swapping or adding a provider is a new file, not a refactor. When one provider had an outage, switching the default was a one-line env change.\n\nAI generation takes 10s–minutes. Blocking a request is a non-starter. The flow:\n\n`generate()`\n\n— create a DB record, `handleCallback()`\n\n— download the result, re-upload to R2, mark complete, The frontend just polls a lightweight status endpoint. The webhook is the source of truth.\n\nA gotcha: **always re-upload the provider's output to your own storage.** Provider URLs expire. Downloading and pushing to R2 on completion saved me from dead links later.\n\nMoney + concurrency + async failures = the scariest combination. The pattern that worked: **freeze → settle / release.**\n\n`freeze(credits)`\n\n— move credits to a \"held\" state`settle()`\n\n— actually consume them`release()`\n\n— give them back\n\n``` php\nfreeze  -> hold created, balance reserved\nsettle  -> hold consumed (success)\nrelease -> hold returned (failure)\n```\n\nThis way a failed generation never costs the user, and a user can't fire 10 concurrent jobs with credits for one. I also did **FIFO consumption across credit packages** so credits with the nearest expiry get used first — fairer for users and simpler for accounting.\n\nIf you want to see the end result, it's live at [aiclotheschanger.me](https://aiclotheschanger.me/). Happy to answer questions about the architecture in the comments.", "url": "https://wpnews.pro/news/building-an-ai-clothes-changer-provider-abstraction-async-jobs-and-a-credit-that", "canonical_source": "https://dev.to/gxlbfc_d039fe229d0c50aa9e/building-an-ai-clothes-changer-provider-abstraction-async-jobs-and-a-credit-system-that-wont-2cc1", "published_at": "2026-06-17 08:16:53+00:00", "updated_at": "2026-06-17 08:51:24.783167+00:00", "lang": "en", "topics": ["artificial-intelligence", "generative-ai", "developer-tools", "ai-products"], "entities": ["Dressora", "Evolink", "R2"], "alternates": {"html": "https://wpnews.pro/news/building-an-ai-clothes-changer-provider-abstraction-async-jobs-and-a-credit-that", "markdown": "https://wpnews.pro/news/building-an-ai-clothes-changer-provider-abstraction-async-jobs-and-a-credit-that.md", "text": "https://wpnews.pro/news/building-an-ai-clothes-changer-provider-abstraction-async-jobs-and-a-credit-that.txt", "jsonld": "https://wpnews.pro/news/building-an-ai-clothes-changer-provider-abstraction-async-jobs-and-a-credit-that.jsonld"}}