Blog
Building with AI September 30, 2026 · EvalyMe

Checkout Is Not Access: Server-Authoritative Billing in AI-Built Apps

Payment providers confirm purchases asynchronously, so a completed checkout proves nothing yet. Here is the entitlement architecture that keeps AI-built apps from leaking paid features or locking out paying users.

entitlements in-app purchases RevenueCat payments

The most expensive race condition in your app

Ask an AI agent to add subscriptions and it will build you a beautiful paywall, a checkout flow, and a local flag that flips to “pro” when the purchase callback fires. In the demo, the user pays, the flag flips, the features unlock. Ship that and you have built a race condition with money on both sides of it.

Payment confirmation is asynchronous and eventually consistent. The store’s webhook may arrive seconds or minutes after the client’s callback. Verification services have latency and occasional outages. If your client decides access at checkout-completion time, you will simultaneously lock out paying users (entitlement not yet propagated) and grant free access to non-payers (cached or forged client state). The first costs you refunds and one-star reviews; the second costs you revenue silently, which is worse because you cannot count what you never see.

The rule we build by: checkout completion is not access. Access is a server-side fact, re-verified at the moment it matters, from the one authority that owns it.

One identity, one authority, every surface

Our product spans iOS, Android, and a web app, all sharing one backend. The billing architecture that has survived contact with reality rests on three decisions:

One identity everywhere. The verified auth user id — Clerk’s, in our case — is the payment provider’s customer id on every platform and on the backend. iOS, Android, and web purchases all land on the same customer record because they all claim the same identity. The alternative, provider-generated anonymous identities that merge later, produces split purchase histories and support tickets you cannot resolve.

Never create anonymous paid identities for signed-out users. An agent will cheerfully initialize the billing SDK on app launch for a signed-out visitor, because that is the provider’s default quickstart. Resist it: billing configuration in our apps waits for a real signed-in user id. The cost of waiting is one line of sequencing; the cost of not waiting is anonymous customers whose purchases attach to nobody.

The server is the only enforcement point. Client-side paywall checks are UX — they make the upgrade prompt polite and instant. The backend independently verifies the entitlement with the payment provider (RevenueCat, in our stack) before any paid operation, keyed by the verified user id from the auth token. If verification is uncertain, paid operations fail closed and the interface degrades to the free tier. A jailbroken client, a patched app binary, or a stale local flag changes nothing, because none of them is the one asking the provider.

The propagation gap and how to survive it

The hardest moment in the flow is the ninety seconds after a successful purchase, when the client knows the user paid but the entitlement has not finished propagating.

What fails here: the app asks once, gets “not entitled,” concludes the purchase failed, and prompts the user to buy again — the classic double-prompt that generates duplicate purchases and chargebacks.

What works instead: after checkout, the client retries backend entitlement verification before declaring success, and preserves a pending-purchase state until verification lands. While pending, the UI must not show the paywall again. The authoritative check remains a server endpoint the client refreshes from; the provider’s cached customer info is used only for responsive UI, never as the decision-maker.

This is also why the failure mode needs to be asymmetric by design: uncertain verification degrades to free-tier presentation while paid server operations fail closed. Users tolerate “checking your access” for a minute. They do not tolerate being charged and locked out, and you cannot tolerate granting paid compute on an unverified claim.

Where this breaks: the false reads

False read one: “the paywall gate is the enforcement.” Founders port the gate object (AssessmentGate in our codebases) and believe the feature is protected. A client gate is a sign, not a lock — our own backend still accepts the underlying data operations because data integrity should not depend on billing state. Only server-checked entitlements gate what actually costs money.

False read two: “sandbox keys are fine for now.” Payment SDK keys come in platform-specific flavors, and using the wrong one fails in ways that look like flaky purchases rather than misconfiguration. The fix is mechanical: our web production build validator rejects sandbox-prefixed keys outright, and release builds refuse to ship with them. If a wrong key can only be caught by a human remembering to look, it will eventually ship.

False read three: “the webhook will handle it.” Webhooks are a channel, not a plan. They arrive late, arrive duplicated, and occasionally do not arrive. Every design decision above — polling the authoritative endpoint, pending states, fail-closed server checks — exists because the webhook is not a guarantee.

A note on the credentials themselves

The billing layer is also where AI-written code most often leaks secrets: keys embedded client-side, or a service key pasted into a codebase because the agent needed it now. Scans of AI-built apps have found hundreds of exposed API keys, and billing provider keys are the most valuable kind. The mechanical rules that hold: only public SDK keys ever ship in clients, secret keys live only in server environment variables, and CI rejects configurations that violate either. Our instruction files name the exact key prefixes allowed per platform so agents cannot quietly substitute a secret one to make an error go away.

Checklist: entitlements that survive real users

  • One verified user id is the payment customer id on every platform, web included.
  • Billing SDK initializes only after sign-in; no anonymous paid identities.
  • Server verifies entitlement with the provider before each paid operation; client checks are UX only.
  • Uncertain verification degrades to free-tier UI and fails closed for paid operations.
  • After checkout, retry server verification under a pending state; never re-prompt while pending.
  • Production builds mechanically reject sandbox or wrong-platform payment keys.
  • Restoring purchases on a new device follows the same server-verified path as the original purchase.

None of this is clever, which is the point. Every item above converts a class of money-losing failure into a build failure. Your AI agent can implement all of it — once you specify which side of each race it should be on.