The rules below govern the v2 wallet contract when a call goes wrong. The cases are a timeout, a duplicate, a rollback of an operation you never saw, and a win that arrives after the session closed. They’re additive: GA removes or renames nothing published in proto/ or openapi/.
Conventions.
- Normative keywords (MUST, MUST NOT, SHOULD, MAY) are used as in Request for Comments (RFC) 2119.
- Error codes are written in prose without the
ERROR_CODE_prefix. On the wire they use the published enum form (for exampleERROR_CODE_ALREADY_ROLLED_BACK). The same applies toRECONCILE_STATE_*,CREDIT_KIND_*,DEBIT_MODE_*andORIGINAL_KIND_*values. - A business refusal travels on HTTP 200 with
{"status":"error","error":{code,message,retryable,retry_after,attributes}}. A success travels as{"status":"ok","data":{…}}. - Request examples are reduced to the relevant envelope fields (
ts,operator_id,correlationand signature headers are omitted unless relevant). All values are illustrative.
1. Summary
| # | Rule |
|---|---|
| 1 | Rollback of an unknown original is a success with original_found=false. The operator remembers the original’s id. A late Debit/Credit with that id gets ALREADY_ROLLED_BACK. The operator never returns UNKNOWN_ORIGINAL to a Rollback |
| 2 | Mandatory order of checks at the operator |
| 3 | New enum value IDEMPOTENCY_CONFLICT and the fingerprint field list |
| 4 | The operator never refuses Credit, SettleGrant, and Rollback for session, player block, or limits. Closed list of terminal Credit refusals |
| 5 | Reconcile for an unknown operation returns an ok envelope + NOT_APPLIED. It’s a read, never a barrier |
| 6 | Concurrent duplicates: the second request waits for the commit and receives the first response |
| 7 | Numbers: op_id retention, GA attempts, resend schedule and horizon, signature window |
| 8 | Zero amount, exponent mismatch, int64 overflow, unknown fields, and unknown enum values |
| 9 | Rollback must match the original’s player and currency. DebitCredit checks the stake first and is all-or-nothing. Balance-reducing corrections arrive as an ordinary Debit |
| 10 | Free rounds and bonus-type credits: what the operator receives, the token rule, no bet required for promo payouts, accrued wins are always settled |
| 11 | What GA does on an unknown outcome, per operation type |
| 12 | Deliberate differences from other aggregators |
2. Items
2.1 Rollback of an unknown original
Normative text
- If the operator holds no record of
original_op_id, a Rollback MUST be answered with a success envelope, MUST move no money, and MUST carryoriginal_found=falseand the player’s current balance. - The operator MUST remember the ORIGINAL’s id (
original_op_id) as rolled back. This is the barrier. Its key is the original’s id, not the Rollback’sop_id. - A Debit MUST check the barrier in the same atomic step that records the debit, before any money moves. The operator serialises a Debit and the Rollback of that Debit on the ORIGINAL’s id.
- The operator MUST refuse a later Debit or Credit that carries that
op_idwithALREADY_ROLLED_BACK, regardless of its fingerprint. - A Rollback naming an original that the operator is still processing MUST get an error envelope with
retryable=true. GA re-drives it until the original resolves. - The operator MUST NOT return
UNKNOWN_ORIGINALto a Rollback. The value stays in the enum. - A repeated Rollback of the same original MUST return the original response and MUST NOT reverse money again.
Example—the operator has never seen op-bet-90:
{
"request_id": "req-0002",
"action": "rollback",
"payload": {
"meta": {
"request_id": "req-0002",
"op_id": "op-rb-91",
"round_id": "rnd-55"
},
"original_op_id": "op-bet-90",
"original_kind": "ORIGINAL_KIND_DEBIT",
"money": {
"currency": "EUR",
"amount": 1000,
"exponent": 2
}
}
}
{
"status": "ok",
"data": {
"operator_tx_id": "tx-8841",
"balance_after": {
"currency": "EUR",
"amount": 25000,
"exponent": 2
},
"original_found": false
}
}
The Debit with op_id op-bet-90 then arrives late:
{
"status": "error",
"error": {
"code": "ERROR_CODE_ALREADY_ROLLED_BACK",
"message": "operation was rolled back",
"retryable": false
}
}
2.2 Mandatory order of checks
Normative text
- The operator MUST evaluate checks in this order: signature → schema →
op_idlookup (replay / conflict / already rolled back) → player + currency/exponent → [Debit/DebitCredit only: session, player status, limits, balance] → round state → apply and store the result atomically. - The
op_idlookup MUST precede the session, player-status, limit, and balance checks. - A duplicate of an applied Debit MUST return the stored success, even when the balance is now insufficient or the session has expired.
- A signature or time failure is a transport-level 401/403 without a code.
- For a new Debit, an expired session is
SESSION_EXPIRED.
Example—a stake of 10.00 took the balance to 0.00. GA resends the same Debit:
{
"request_id": "req-0011",
"action": "debit",
"payload": {
"meta": {
"request_id": "req-0011",
"op_id": "op-bet-77",
"round_id": "rnd-40",
"launch_token": "lt-abc"
},
"money": {
"currency": "EUR",
"amount": 1000,
"exponent": 2
}
}
}
{
"status": "ok",
"data": {
"operator_tx_id": "tx-9001",
"balance_after": {
"currency": "EUR",
"amount": 0,
"exponent": 2
}
}
}
This response is identical to the response to the first request.
2.3 IDEMPOTENCY_CONFLICT
Normative text
- The closed error enum has 15 codes plus the
ERROR_CODE_UNSPECIFIEDdefault.IDEMPOTENCY_CONFLICTis the only code added after the initial publication. - For the same
op_idwith a different fingerprint, the operator MUST refuse withIDEMPOTENCY_CONFLICT, and no money moves. - The fingerprint is: player, operation type, currency, amount together with exponent, round, original, grant reference.
- This applies while the operation is APPLIED. An
op_idthat the operator has rolled back, or remembers as rolled back, answers per item 2.1, whatever the fingerprint. - The same
op_idwith the same fingerprint returns the stored success.
Example—the operator applied op-bet-77 for 10.00. GA now sends the same op_id with 20.00:
{
"request_id": "req-0031",
"action": "debit",
"payload": {
"meta": {
"request_id": "req-0031",
"op_id": "op-bet-77",
"round_id": "rnd-40",
"launch_token": "lt-abc"
},
"money": {
"currency": "EUR",
"amount": 2000,
"exponent": 2
}
}
}
{
"status": "error",
"error": {
"code": "ERROR_CODE_IDEMPOTENCY_CONFLICT",
"message": "op_id reused with a different fingerprint",
"retryable": false
}
}
2.4 What’s never refused
Normative text
- The operator MUST NOT refuse Credit and Rollback because the session or token has expired. GA still sends the launch token of that session. Only its expiry isn’t a reason to refuse.
- A blocked or disabled player can’t place NEW bets. The operator MUST still accept Credit and Rollback for such a player.
- The operator MUST NOT refuse Credit and Rollback for limits.
- SettleGrant is a money operation like a Credit and follows the same rule: the operator MUST NOT refuse it for session, player block, or limits.
- The terminal refusals of a Credit are a closed list:
- player not found (
PLAYER_NOT_FOUND) - currency mismatch (
CURRENCY_MISMATCH) - invalid request (
INVALID_REQUEST) IDEMPOTENCY_CONFLICT- “the bet this win refers to is unknown to the operator or was rolled back” (
UNKNOWN_ORIGINAL/ALREADY_ROLLED_BACK)
- player not found (
- After any other refusal of a Credit, GA resends it.
- Round state: the operator refuses a new Debit in a CLOSED or VOIDED round with
BUSINESS_REJECTED. It accepts a Credit in OPEN rounds and, as a late win, in CLOSED rounds. It refuses a Credit in a VOIDED round withALREADY_ROLLED_BACK. It accepts Rollback and Reconcile in any round state.
Example—the player’s session ended hours ago, but the win still arrives:
{
"request_id": "req-0042",
"action": "credit",
"payload": {
"meta": {
"request_id": "req-0042",
"op_id": "op-win-77",
"round_id": "rnd-40",
"launch_token": "lt-abc"
},
"money": {
"currency": "EUR",
"amount": 2500,
"exponent": 2
},
"kind": "CREDIT_KIND_WIN",
"round_finish": true
}
}
{
"status": "ok",
"data": {
"operator_tx_id": "tx-9002",
"balance_after": {
"currency": "EUR",
"amount": 2500,
"exponent": 2
}
}
}
2.5 Reconcile
Normative text
-
Reconcile is a read. It MUST always return an ok envelope.
-
The operator derives the state from its record of
original_op_id:Operator’s record Reconcile state absent NOT_APPLIEDstill being committed UNKNOWNapplied APPLIEDrolled back / remembered as rolled back NOT_APPLIED -
Reconcile MUST NOT record anything. It’s never a barrier.
-
The operator MUST NOT return
UNKNOWN_ORIGINALto a Reconcile. -
For
NOT_APPLIEDof a Credit, GA’s reading is “resend it”.
Example—the operator never saw op-bet-90:
{
"request_id": "req-0050",
"action": "reconcile",
"payload": {
"meta": {
"request_id": "req-0050",
"op_id": "op-rc-12"
},
"original_op_id": "op-bet-90"
}
}
{
"status": "ok",
"data": {
"state": "RECONCILE_STATE_NOT_APPLIED"
}
}
The operator processes a later Debit with op_id op-bet-90 normally, because the Reconcile recorded nothing.
2.6 Concurrent duplicates
Normative text
- The operator MUST serialise per
op_id: it inserts a unique key before business logic runs. - The second request MUST wait for the commit of the first.
- If its fingerprint matches, the second request MUST receive the same response as the first. Otherwise it receives
IDEMPOTENCY_CONFLICT. - The operator never applies the operation a second time.
- The “same response” rule concerns operations that WERE processed. A refused request leaves the
op_idunused, and the operator MAY evaluate it again when resent. GA never resends a Debit after your business refusal. GA resends a refused Credit until you accept it. If an earlier attempt of the same Debit ended without an answer, GA treats the Debit as unknown even after a later refusal and sends a Rollback. Keep the barrier (2.4): the earlier request may still reach you.
Example—two requests arrive at once. Both carry op_id op-bet-88, with request_id req-0061 and req-0062. Both receive:
{
"status": "ok",
"data": {
"operator_tx_id": "tx-9100",
"balance_after": {
"currency": "EUR",
"amount": 4000,
"exponent": 2
}
}
}
The balance changed once.
2.7 Numbers
Normative text
- Both sides MUST keep
op_idrecords for at least 4 months. The 7-day window of the published v1 wallet spec remains the hard minimum for v1 bridges. - For a Debit/DebitCredit in unknown state, GA makes up to 4 quick attempts with the same
op_id, 100 ms, 500 ms, and 2 s apart. ARetry-Aftermay stretch a pause up to 10 s. Each attempt waits at most 5 s. All attempts share the operation budget of item 2.11.7 (8 s by default), so a silent operator gets 2 attempts: 5 s, then the rest of the budget. - For a Rollback, and on the same schedule for a Credit/SettleGrant in unknown state, GA resends for up to 72 hours. The backoff is exponential (1 s, doubling, capped at 10 minutes). GA stops at the first logical, valid-envelope response. The limit is the 72-hour horizon, not a number of attempts. This is inside the 7-day v1 window and inside the 4-month retention.
- When the 72 hours are over, GA sets
manual_reviewand raises an alert. The state stays unknown, and GA never marks it “failed”. - Every attempt carries a new
request_idand the sameop_id. - The signature window is 300 s:
X-GA-Signature: t=,v1=, rejected when|now − t| > 300. The failure is a transport-level 401/403 without a code.
Example—attempt 2 of a Credit resend (new request_id, same op_id):
X-GA-Signature: t=1790000000,v1=<hex>
{
"request_id": "req-0072",
"action": "credit",
"payload": {
"meta": {
"request_id": "req-0072",
"op_id": "op-win-91",
"round_id": "rnd-60",
"launch_token": "lt-abc"
},
"money": {
"currency": "EUR",
"amount": 500,
"exponent": 2
},
"kind": "CREDIT_KIND_WIN"
}
}
{
"status": "ok",
"data": {
"operator_tx_id": "tx-9200",
"balance_after": {
"currency": "EUR",
"amount": 1500,
"exponent": 2
}
}
}
This is the stored success of attempt 1, which the operator had committed but GA never received.
2.8 Amount and schema validation
Normative text
- Money is
{currency, amount:int64, exponent:0..18}. - A Debit amount MUST be > 0. A Credit amount MAY be 0 and is then valid. A zero Credit closes a round. A negative amount is
INVALID_REQUEST. - The operator MUST refuse an exponent mismatch or an exponent outside 0..18 with
CURRENCY_MISMATCH. - The operator MUST refuse an int64 overflow with
INVALID_REQUEST. - Both refusals happen before money moves, and the
op_idstays unused. - The operator MUST ignore unknown fields.
- The operator MUST refuse an unknown enum value in a known field with
INVALID_REQUEST.
Example—the operator holds EUR with exponent 2:
{
"request_id": "req-0080",
"action": "debit",
"payload": {
"meta": {
"request_id": "req-0080",
"op_id": "op-bet-95",
"round_id": "rnd-70",
"launch_token": "lt-abc"
},
"money": {
"currency": "EUR",
"amount": 1000,
"exponent": 3
}
}
}
{
"status": "error",
"error": {
"code": "ERROR_CODE_CURRENCY_MISMATCH",
"message": "exponent mismatch",
"retryable": false
}
}
2.9 Rollback matching, DebitCredit, balance-reducing corrections
Normative text
- The operator MUST execute a Rollback only if its player and currency match the original’s. Otherwise it refuses the Rollback terminally with
INVALID_REQUESTorCURRENCY_MISMATCH, and no money moves. - A Rollback of a Debit is always full:
moneyequals the stored amount of the original. A partial rollback exists only for a Credit, throughpartial/debit_mode/debited. DebitCreditMUST check the stake against the current balance FIRST, as for a separate Debit, and only then pay the win of the same call. The operator applies or refuses stake and win together. It’s all-or-nothing.- A provider correction (adjustment) that REDUCES the player’s balance arrives as an ordinary
Debit, idempotent by anop_idthat GA derives from the provider’s own correction id. One that INCREASES it arrives as aCreditof kindADJUSTMENT. The correction names no earlier operation and rolls nothing back. - Apply your usual Debit checks. The only refusal the providers define for a correction is insufficient funds (
INSUFFICIENT_FUNDS). GA relays your refusal to the provider as a refusal and never turns a refused correction into another operation. - The Debit carries no correction marker in v1.0. A correction can arrive outside an active game session, for example in live-casino settlements and tournaments. If you reject Debits on an expired session, you refuse such a correction, and the provider resolves it on its side. The same applies to any other charge a provider sends outside a session: a participation fee, a contribution to a promo pool, a retried bet that the provider flags as offline.
Example—a 30.00 balance-reducing correction:
{
"request_id": "req-0090",
"action": "debit",
"payload": {
"meta": {
"request_id": "req-0090",
"op_id": "op-corr-5",
"round_id": "adj-5531"
},
"money": {
"currency": "EUR",
"amount": 3000,
"exponent": 2
}
}
}
{
"status": "ok",
"data": {
"operator_tx_id": "tx-9300",
"balance_after": {
"currency": "EUR",
"amount": 10000,
"exponent": 2
}
}
}
2.10 Free rounds and bonus-type credits
Normative text
- GA has two free-round mechanisms. They never share a callback.
- Operator-issued grant. The operator calls
IssueGrant/ListGrants/CancelGrant. A round played on a grant sends NO Debit and NO Credit to the operator. The win accumulates on the grant, andSettleGrantpays it ONCE (carryinggrant_id,total_win,rounds_played) when the grant is exhausted. GA settles a zero-win grant too. Grant states are ACTIVE / EXHAUSTED / CANCELLED / EXPIRED / SETTLED. For an operator-issued grant,SettleGrantis the only money call the operator receives. - Provider-issued promo. The operator receives, per winning spin, an ordinary
Creditof kindFREE_ROUNDcarrying the campaign reference (campaign_ref), or oneSettleGrantat the end. IssueGrantis idempotent by the operator’sidempotency_key. The same key returns the same grant. GA refuses the same key with different parameters. Without a key every call creates a new grant, so the operator SHOULD always send one.- You can cancel only an ACTIVE grant. A repeated cancel returns the same result. GA allows cancelling a partly played ACTIVE grant.
- A grant has a hard end date (
valid_until). GA refuses a free round consumed after that date. - GA never drops money already won on a grant. GA pays a win of a round played before the grant ended (cancelled, expired, or settled).
SettleGrantpays it for the rounds actually played. When the win arrives after settlement, a separateCreditof kindFREE_ROUNDcarrying the grant reference pays it. GA refuses to the provider a NEW round reported on a grant that has already ended, and that round never reaches you. - Token rule. For free-round, promo, and bonus credits, the operator MUST NOT validate the EXPIRY of the session/token. They may arrive when the player is offline. The token, when sent, must still be one the operator knows.
- Credits of kind
BONUS,PROMO,JACKPOT,TOURNAMENT,CASHBACK,ADJUSTMENT,FREE_ROUNDare ordinary Credits: items 2.2–2.7 apply in full. They’re idempotent byop_id. The operator never refuses them for session, block, or limits. GA resends them until delivered and never rolls them back. - No bet required. A promo, bonus, jackpot, or tournament payout may come without any bet or debit in the round. The operator MUST NOT require one. An ordinary win belongs to its round as usual.
- The operator answers success to a provider’s rollback of a free-round bet. The rollback moves no money, because the bet took none.
SettleGrantis never a Rollback target. SettleGrantis one per grant and idempotent byop_id. On an unknown outcome, GA resends it per item 2.7 until acknowledged.
Example—a grant is exhausted, and GA settles it once:
{
"request_id": "req-0100",
"action": "settle_grant",
"payload": {
"meta": {
"request_id": "req-0100",
"op_id": "op-sg-3"
},
"grant_id": "grant-2041",
"total_win": {
"currency": "EUR",
"amount": 1800,
"exponent": 2
},
"rounds_played": 20
}
}
{
"status": "ok",
"data": {
"operator_tx_id": "tx-9400",
"balance_after": {
"currency": "EUR",
"amount": 11800,
"exponent": 2
}
}
}
2.11 What GA does on an unknown outcome
Normative text
- An unknown outcome is any of: a timeout, a disconnect, any response that isn’t a valid envelope with a known code (including 401/403/404/5xx from a proxy). A valid envelope with a retryable code (
RATE_LIMITED,MAINTENANCE,INTERNAL) on a money operation is also an unknown outcome. - Debit / DebitCredit.
- GA makes up to 4 quick attempts with the same
op_id, 100 ms, 500 ms, and 2 s apart, inside the budget of item 2.11.7 (numbers in item 2.7). - If the outcome is still unknown, GA fails the bet towards the provider with a retryable error, so the round doesn’t start. GA also generates ONE linked Rollback.
- GA makes up to 4 quick attempts with the same
- Credit / SettleGrant. GA resends with the same
op_iduntil success or until the resend limit of item 2.7 runs out. GA never rolls back a bet because it couldn’t deliver a win. Only the provider may send that rollback. - Rollback.
- GA resends with the same
op_idper item 2.7 (up to 72 hours, exponential backoff, stopping at the first logical response). - A non-retryable refusal, meaning a valid envelope with a non-retryable code, stops resending at once: GA sets
manual_reviewand raises an alert.
- GA resends with the same
- A Rollback is never in flight concurrently with its original. GA records a provider’s cancel that arrives while the Debit is in flight and answers it retryable. GA executes it after the Debit resolves.
- There is one Rollback per original, and GA reads its amount from the stored original.
- An overall operation budget bounds the retry sequence (GA’s own sender setting, 8s by default): the first attempt always goes out. GA starts no retry and waits no backoff the remaining budget can’t hold. It answers the provider retryable with an unknown outcome before the provider’s own timeout expires.
Example—a Debit op-bet-90 gets no valid response within the budget (two attempts on a silent wallet: two request_id values, same op_id). GA fails the bet towards the provider and sends the linked Rollback:
{
"request_id": "req-0205",
"action": "rollback",
"payload": {
"meta": {
"request_id": "req-0205",
"op_id": "op-rb-91",
"round_id": "rnd-55"
},
"original_op_id": "op-bet-90",
"original_kind": "ORIGINAL_KIND_DEBIT",
"money": {
"currency": "EUR",
"amount": 1000,
"exponent": 2
}
}
}
{
"status": "ok",
"data": {
"operator_tx_id": "tx-8841",
"balance_after": {
"currency": "EUR",
"amount": 25000,
"exponent": 2
},
"original_found": false
}
}
If the Debit reaches the operator later, the operator refuses it with ALREADY_ROLLED_BACK (item 2.1).
2.12 Deliberate differences from other aggregators
The published GA contract governs. Some of its choices differ from what other aggregators do. Don’t “fix” them when porting an existing wallet integration:
- A business refusal travels on HTTP 200 with the status in the body. Non-200 statuses are transport-level only.
- An error response carries no balance.
- Money is
int64in minor units plus an explicitexponent, never a fixed scale, and never a decimal string. - A signature failure is a transport-level 401/403 without a body status.
- A partial rollback exists, but only for a Credit (
partial,debit_mode,debited). A rollback of a Debit is always full. - A Credit is never refused for limits or session (item 2.4).
- A round may stay OPEN forever. GA doesn’t require every round to be closed.
- A duplicate returns the stored success (item 2.6), never a conflict status.
- GA doesn’t send free-round bets to the operator (item 2.10). The operator learns the rounds from
SettleGrant.
3. Conformance additions
| Check id | Setup | Action | Expected |
|---|---|---|---|
| W2-DELTA-01 debit after rollback-of-unknown | The operator has no record of op_id D. The player has a known balance B | 1. Rollback with original_op_id = D. 2. Debit with op_id = D: once with the original amount, once with a different amount | 1. ok envelope, original_found=false, balance B, no money moved. 2. Both Debits refused with ALREADY_ROLLED_BACK. Balance still B |
| W2-DELTA-02 duplicate debit when balance is now insufficient | Debit D (stake S) applied. The balance is now below S. Also run with the session expired | The same Debit resent: same op_id, same fingerprint, new request_id | ok envelope identical to the first response (same operator_tx_id and balance_after). Balance unchanged |
| W2-DELTA-03 duplicate with a different amount | Debit D (amount A) applied | Debit with the same op_id and amount A′ ≠ A | IDEMPOTENCY_CONFLICT. Balance unchanged. The stored result of D unchanged |
| W2-DELTA-04 credit after session expiry | Debit applied in a session. The session/token has since expired | Credit for the same round, carrying the launch token of that session | ok envelope. The operator credits the balance exactly once |
| W2-DELTA-05 concurrent duplicates | A fresh op_id, a known balance | Two or more simultaneous identical Debits (different request_id, same op_id) | Every request gets the same ok response. The operator applies exactly one debit |
| W2-DELTA-06 exponent mismatch | The player’s currency has exponent e at the operator | Debit with the same currency and an exponent ≠ e | CURRENCY_MISMATCH. No money moved. The op_id stays unused |