> ## Documentation Index
> Fetch the complete documentation index at: https://docs.playstarters.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Idempotency

> How to safely handle retried wallet callback requests without double-charging or double-crediting players.

All Seamless Wallet calls include a unique `requestId`. Network timeouts, gateway hiccups, or internal PlayStarters retries can cause the **same request** to arrive at your endpoint more than once. Your implementation must handle this without re-applying the financial movement.

## The rule

If you receive a request with a `requestId` you have already processed, **do not** reapply the movement. Return HTTP `200 OK` with the player's **current** balance.

## Recommended implementation

<Steps>
  <Step title="Store processed requestIds">
    On every successful `BET`, `WIN`, or `VOID`, persist the `requestId` together with the resulting balance in a transactions table or cache. Use a unique index/constraint on `requestId` to prevent duplicates at the database level.
  </Step>

  <Step title="Check before applying">
    Before applying a new transaction, look up the `requestId`. If it already exists, skip the movement and return the previously stored balance.
  </Step>

  <Step title="Wrap the read + write in a single transaction">
    The check, the movement, and the `requestId` write must be atomic to avoid race conditions when the same request arrives twice in parallel.
  </Step>
</Steps>

## Example pseudocode

```ts theme={null}
async function handleWalletCallback(req) {
  return db.transaction(async (tx) => {
    const existing = await tx.transactions.find(req.requestId);
    if (existing) {
      return { balance: existing.balanceAfter };
    }

    if (req.type === "BALANCE") {
      const balance = await tx.players.getBalance(req.playerId);
      return { balance };
    }

    // BET / WIN / VOID
    const balance = await tx.players.applyMovement(req);
    await tx.transactions.insert({
      requestId: req.requestId,
      playerId: req.playerId,
      type: req.type,
      amount: req.amount,
      balanceAfter: balance,
    });
    return { balance };
  });
}
```

<Warning>
  Idempotency is **mandatory**. Without it, a single retried `BET` can debit the player twice, or a retried `WIN` can credit them twice — both create reconciliation issues that are painful to unwind.
</Warning>
