Skip to main content

The requirement

All transaction endpoints must be idempotent. If the same transactionId arrives again, do not apply the operation a second time — return 200 OK with valid data.
This is not a theoretical safeguard. Duplicates are a normal part of operating the integration: a response times out on the network while your server processed it fine, and the retry lands on a transaction you already committed.

The key

transactionId is your idempotency key. It is generated by Zero-Dash, unique per transaction, and present on every money-moving callback.
Key on transactionId, not on gameRoundId. A single round produces at least two transactions — the debit and its settlement — and they share a round ID. Deduplicating by round would drop the settlement.

Implementing it

The database does the work. A unique constraint plus one atomic transaction is both correct and fast; application-level “check then write” is a race waiting to happen.

Constrain the ledger

transaction_id as the primary key makes a duplicate physically impossible.

Insert and update atomically

One transaction. If either statement fails, neither takes effect.

Replay the stored result on conflict

Return that row as a normal 200 OK. Same shape, same values, no second movement.
Return the balance stored with the original transaction, not the player’s current balance. Later rounds may have changed it, and replaying a duplicate should reproduce the original answer.

A reference handler

Idempotency and rollback

A rollback is only valid against a bet that exists.
Never let a rollback create a credit out of thin air. If referenceTransactionId does not exist in your ledger, return 404. Blindly crediting a stake for an unknown bet is how a duplicate-delivery bug becomes a money leak.

Testing it

Send an identical debit twice. The balance must move once, and both responses must be identical 200 OKs.
Fire both at the same instant, on different connections. This is what the unique constraint is for — an application-level check-then-write will apply both.
Debit, settle, play another round, then replay the first debit. The response must carry the original balanceAfter, not the current balance.
A losing round credits 0. Replaying it must still be a no-op and still return 200 OK.