Idempotency
Prevent double debit and double credit in wallet processing
Wallet callbacks can be delivered more than once. If your backend processes the same tx_id twice, players can be charged or credited incorrectly.
Required Behavior
For every callback:
- Verify the Phoenix signature.
- Start a database transaction.
- Look up
tx_idin your wallet transaction table. - If it already exists, return the stored result without changing balance again.
- Lock the player balance row or use an atomic balance update.
- Apply the debit, credit, or rollback once.
- Store the
tx_id, request body, result, and final balance. - Commit.
Debit
Debit is synchronous. The player is waiting for the bet result.
If Phoenix receives a clear transport error before your service processes the debit, Phoenix may retry briefly. If Phoenix times out, it does not keep retrying inline because your backend may still process the first request.
Your side should still be safe if the same debit tx_id appears again.
Credit
Credit happens after settlement. Phoenix retries temporary delivery failures with backoff.
If you receive the same credit tx_id more than once, return success without crediting the player again.
Rollback
Rollback reverses a previous debit. It must be idempotent independently from the debit.
Your ledger should make these states clear:
| State | Meaning |
|---|---|
| Debit accepted | Player funds were reserved or deducted |
| Debit rejected | Bet was not accepted |
| Credit applied | Settlement payout was recorded |
| Rollback applied | Previously accepted debit was refunded |
SQL Pattern
BEGIN;
SELECT * FROM wallet_transactions WHERE tx_id = $1 FOR UPDATE;
-- if found, return stored response
SELECT balance FROM players WHERE id = $2 FOR UPDATE;
-- validate and update balance once
INSERT INTO wallet_transactions (
tx_id,
player_id,
kind,
amount_cents,
response_json
) VALUES ($1, $2, $3, $4, $5);
COMMIT;Use your own schema, but keep the same behavior: one tx_id, one balance effect.