All posts
guide

One click, one paid AI action: prevent duplicate requests

A durable operation record helps repeated clicks return to one action. Unknown upstream outcomes need reconciliation, not an automatic paid retry.

EiliyaOctober 1, 20264 min read

A user clicks Generate. Nothing appears immediately, so they click again. Then the page refreshes. Your app now has several signals for what the user thought was one purchase.

Disabling the button helps, but it does not settle requests already in flight, a second browser tab, or a network timeout after the model started working. For a paid AI feature, "one click, one paid action" should be a product invariant you design toward, not a promise you make based on a spinner.

AI Pass provides wallet-funded OAuth access to model usage. That funding route makes duplicate dispatch particularly visible to users, but provider-key BYOK and app-funded requests need the same care.

Give the action an identity before sending it

Create a local operation record when the user confirms a specific action. Attach the app user, input version, model, settings, and selected funding route. Give the operation a unique reference that survives a page refresh.

A reference is useful for your own deduplication and support. It is not proof that the upstream generation API recognizes it. Check the exact endpoint contract in the REST documentation or SDK documentation before relying on an idempotency header or retry behavior.

Do not infer generation guarantees from another endpoint. For example, provisioning a client and generating an image are different operations. The integration guide describes setup behavior that should not be generalized into universal model-call idempotency.

A sequence for a backend-controlled action

The following is an application design, not an AI Pass API specification:

  1. The user reviews the task and funding source, then confirms Generate.
  2. The browser submits the operation reference and input version to your backend.
  3. The backend atomically creates or claims the operation. A repeated submission reads the existing record instead of starting another worker.
  4. The worker records that dispatch is beginning and makes the model request through the selected route.
  5. The app saves any returned request reference, then stores the result durably when available.
  6. The browser reads the operation state and displays the saved result.
  7. Reloads and repeated button presses return to that operation rather than creating another one.

Use a database constraint or transaction for the claim. A JavaScript variable in one server process will not coordinate multiple instances. Bind the record to the authenticated app user so an operation reference cannot expose someone else's result.

For a browser-only SDK app, local state can prevent common repeated clicks, but it cannot provide the same coordination across devices. Describe that limitation honestly and choose a supported architecture that fits your reliability needs.

Decide what to do after uncertainty

Observation Meaning App decision
Repeat submission finds an active operation The app has already accepted this action Return its status; do not dispatch again
Saved result exists The app has completed this operation Display that result
Failure happened before dispatch No upstream request was sent by this attempt Offer a retry after fixing the cause
Endpoint definitively rejected the request before work began This attempt did not begin generation under that response contract Offer a corrected attempt
Connection dropped after dispatch The model may have processed the request Mark outcome unknown and reconcile
Supported status lookup confirms completion A result or completion record is available Recover it through the documented path
No authoritative status is available The app cannot establish what happened Explain uncertainty; avoid an automatic paid retry

A timeout is an observation about communication. It is not a receipt proving the request failed or cost nothing.

Handle the worker crash window

The awkward case is a worker that sends the request and crashes before saving the response. Its operation record may say "dispatching" forever unless you have a recovery process.

If the selected endpoint supports authoritative status lookup or documented idempotent replay, use it within its stated limits. Otherwise, move the operation to an unknown state for review. Do not release an expired worker lock and assume a fresh worker can safely generate again.

Show: "We lost the connection after sending this request. It may have completed. We're checking before offering another attempt." If checking cannot resolve it, explain that a new attempt could create another charge and let the user decide.

Separate another version from a repeated click

A deliberate "Generate another version" should create a new operation after a fresh user action. Reopening the same result should not. Keep the original input and completed output available so users have a reason to choose another version beyond uncertainty about whether the first click worked.

Test double-clicks, refresh during dispatch, two tabs, and a worker crash after sending. Count outgoing model requests in each test. A disabled button is a useful interface detail; the operation record and recovery decisions determine whether the paid behavior matches it.