A retry is not a second purchase decision
Purchase flows fail in the space between an external payment event and an in-game grant. A receipt can arrive twice. A network request can time out after the server committed state but before the client received the answer. A process can crash after reserving a transaction but before granting currency. On restart, the safest action may be to retry work that was interrupted. If the system treats every retry as permission to grant, duplicate entitlements are a normal consequence of recovery.
The useful distinction is intent versus grant. Intent is the request to obtain a specific product under a specific transaction identity. Grant is the durable game-side outcome: entitlement ownership, a currency delta, an inventory item, or a subscription period. The same intent may be delivered, queried, retried, or redelivered many times. It should still resolve to one grant decision.
This is an engineering pattern, not a platform-specific guarantee. Different stores, payment providers, and game backends expose different receipt, acknowledgement, and refund behavior. Do not assume a receipt proves a grant, or that an acknowledgement proves the player saw an item. Build the server-side record so the game can state what it knows and recover safely when an external result is incomplete or delayed.
Give each purchase intent a stable key
An idempotency key binds repeated delivery attempts to one decision. It can derive from a trusted platform transaction identifier when that identifier is available and verified, or it can be issued by a server before a purchase begins and linked later to trusted receipt information. The key must be stable across retries. Creating a new random key every time the client presses “restore” simply creates multiple transactions that happen to look similar.
Store enough context with the key to reject mismatches: player or account scope, product identity, expected quantity or entitlement shape, source channel, received time, verification status, and a state such as pending, committed, rejected, or needs-review. Keep sensitive receipt material out of ordinary logs. If raw provider payloads need short retention for a verified recovery flow, protect them separately and make their access deliberate.
The data model also needs a uniqueness boundary. For a non-consumable entitlement, uniqueness might be account plus product ID. For currency, uniqueness is usually the ledger entry or grant transaction rather than the current balance. For consumables, a trusted transaction identifier may be unique even when the product ID repeats. Define the key before coding the retry policy; otherwise two subsystems may deduplicate against incompatible identities.
Model the unknown commit result
The difficult outcome is not a clean success or a clean failure. It is “unknown”: the client does not know whether the server committed the grant. A timeout, connection reset, process restart, or service outage can produce this state. The safe client behavior is to ask for the transaction status or retry the same intent. The safe server behavior is to look up that intent and return the durable decision rather than applying another mutation.
Do not make the client’s display state authoritative. A spinner disappearing, a local receipt cache, or an optimistic balance change may improve experience, but it cannot decide whether the account owns an entitlement. Server authority means the server verifies the accepted transaction according to the product’s trust boundary, commits the durable record once, and returns the current state when asked again.

The sequence should make the partial-completion policy visible. For example: receive intent, validate trusted purchase evidence, create or load the durable ledger record, commit the entitlement or currency mutation, mark the record granted, acknowledge external delivery when appropriate, then return the result. If the process stops between two steps, a recovery worker or later status query can resume from the durable record. The exact ordering depends on the external provider and game architecture, but the single grant identity does not.
Protect entitlement and currency invariants
An idempotency key prevents replay of one intent; it does not automatically make every side effect correct. Define the invariants a grant must preserve. A non-consumable entitlement appears at most once for the account and product. A currency grant has a ledger entry with one transaction key, a known delta, and a resulting balance that matches the sum of valid entries. Inventory grants obey stack, capacity, and item-definition rules. A subscription period does not extend twice because the confirmation endpoint ran twice.
Make the invariant check part of the same durable operation that commits the grant. A read followed by a later write can race under concurrent delivery. Two workers can both observe “not owned” and both grant. Use transactional storage, compare-and-set semantics, a serialized account mutation path, or another explicit concurrency primitive suited to the data store. The important property is that the decision and its marker cannot be independently won by two attempts.
For games with an in-memory runtime, distinguish durable success from presentation. The server may update a player’s visible balance after the durable ledger entry commits, but the visible update is not the proof. On reconnect or restart, rebuild the presented state from the durable account record. That makes a delayed client response inconvenient rather than financially unsafe.
Test crashes, restarts, and redelivery deliberately
Start with a deterministic scenario. Create one account, one product, one known starting balance or entitlement set, and one stable transaction key. Run the flow, then inject a failure after each named boundary: after receipt acceptance, after ledger creation, after entitlement mutation, after status marking, after external acknowledgement, and after the response is prepared. Restart the relevant worker or process, retry the same key, and assert the final durable state.
The expected outcome should be specific. A retry can return “already granted” with the original result, resume a pending operation, reject a mismatched payload, or hold a record for review. It should not add a second inventory item, apply a second currency delta, or conceal the unknown state with a fabricated success. Capture the classification in the test result so a later refactor cannot turn an intentional hold into an accidental duplicate.
Use receipt redelivery in the same scenario. Deliver the exact trusted transaction twice, then deliver it after a restart, then deliver it alongside a duplicate client retry. The resulting ledger and account state should be identical to the single-delivery case. If the product permits repeat purchases, run the same sequence with two genuinely distinct transaction identities to prove the system does not over-deduplicate by product ID alone.
Keep the boundaries honest
Testing can prove only the interactions you execute. A simulated provider response can validate your state machine, but it is not evidence that every production provider edge behaves exactly the same way. An executable test against a sandbox can add confidence for that integration, yet still has different operational conditions from a live store. Record whether a scenario uses a simulation, local adapter, sandbox, or verified provider environment.
The trust boundary should also be clear. A client should not submit an arbitrary amount, product ID, or “verified” flag and receive a grant. Inputs are untrusted until the server validates them against the relevant configuration and purchase evidence. If validation cannot finish, keep the intent pending or reject it with a recoverable category. Do not let a retry queue become a bypass around verification.
Privacy applies to this testing discipline. Fixtures should use synthetic transaction data. Logs should reference a safe test identifier, not a raw receipt or account name. A production incident may require restricted evidence, but the routine test suite should never become a second payment-data store.
Practical checklist
- Separate a stable purchase intent from the later game-side grant.
- Use one idempotency key across timeouts, retries, restarts, and receipt redelivery.
- Bind the key to the account, product or entitlement shape, and trusted source context.
- Define durable states such as pending, committed, rejected, and needs-review.
- Enforce entitlement uniqueness and currency-ledger invariants inside the same concurrency boundary as the grant.
- Inject failures after each state transition, then restart and retry the same intent.
- Test duplicate delivery and legitimate repeat purchases separately.
- Report unknown outcomes honestly; do not transform a timeout into an invented success.
- Keep receipt-like fixtures and test logs synthetic and redacted.
Frequently asked questions
Is a unique receipt ID enough for idempotency?
It can be a useful component when the server can verify that the receipt identity is trustworthy and scope it correctly. It is still important to store the resulting grant decision, mismatch policy, and recovery state. A system may receive the same identifier repeatedly, receive it late, or receive it with a payload that does not match the stored product context. The durable record is what makes those cases inspectable.
Should a client retry automatically after a timeout?
Usually it should retry status resolution using the same intent identity, with bounded backoff and clear user feedback. Whether it should resubmit external purchase evidence depends on the provider and verification design. The unsafe behavior is silently creating a new purchase request or applying a local grant because the original response was lost.
How do consumables differ from permanent entitlements?
Permanent entitlements usually deduplicate by account and product. Consumables may allow the same product to be bought many times, so the uniqueness boundary must include a transaction identity or ledger sequence rather than product ID alone. Both still need a single durable grant for each accepted intent.
What does Persistium claim here?
Persistium is in development. This note explains broadly applicable integrity and test-design patterns; it does not claim a public purchase product, platform certification, customer incident history, or a hosted service. Its Continuity direction is interested in the same kind of bounded evidence and replay-safe recovery discipline.








