Building an AI Clothes Changer: provider abstraction, async jobs, and a credit system that won't lose money 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. 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. AI providers change pricing, rate limits, and quality constantly. Hardcoding one is a trap. I put everything behind a small factory: js const provider = getProvider "evolink" ; const task = await provider.createTask { prompt, aspectRatio } ; Each provider implements the same interface createTask , handleCallback , 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. AI generation takes 10s–minutes. Blocking a request is a non-starter. The flow: generate — create a DB record, handleCallback — download the result, re-upload to R2, mark complete, The frontend just polls a lightweight status endpoint. The webhook is the source of truth. A 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. Money + concurrency + async failures = the scariest combination. The pattern that worked: freeze → settle / release. freeze credits — move credits to a "held" state settle — actually consume them release — give them back php freeze - hold created, balance reserved settle - hold consumed success release - hold returned failure This 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. If 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.