# Money Path Rules

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 example `ERROR_CODE_ALREADY_ROLLED_BACK`). The same applies to `RECONCILE_STATE_*`, `CREDIT_KIND_*`, `DEBIT_MODE_*` and `ORIGINAL_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`, `correlation` and 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**

1. 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 carry `original_found=false` and the player's current balance.
2. 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's `op_id`.
3. 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.
4. The operator MUST refuse a later Debit or Credit that carries that `op_id` with `ALREADY_ROLLED_BACK`, regardless of its fingerprint.
5. 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.
6. The operator MUST NOT return `UNKNOWN_ORIGINAL` to a Rollback. The value stays in the enum.
7. 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`:

```json
{
  "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
    }
  }
}
```
```json
{
  "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:

```json
{
  "status": "error",
  "error": {
    "code": "ERROR_CODE_ALREADY_ROLLED_BACK",
    "message": "operation was rolled back",
    "retryable": false
  }
}
```

---

### 2.2 Mandatory order of checks

**Normative text**

1. The operator MUST evaluate checks in this order: signature → schema → `op_id` lookup (replay / conflict / already rolled back) → player + currency/exponent → [Debit/DebitCredit only: session, player status, limits, balance] → round state → apply and store the result atomically.
2. The `op_id` lookup MUST precede the session, player-status, limit, and balance checks.
3. A duplicate of an applied Debit MUST return the stored success, even when the balance is now insufficient or the session has expired.
4. A signature or time failure is a transport-level 401/403 without a code.
5. 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:

```json
{
  "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
    }
  }
}
```
```json
{
  "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**

1. The closed error enum has 15 codes plus the `ERROR_CODE_UNSPECIFIED` default. `IDEMPOTENCY_CONFLICT` is the only code added after the initial publication.
2. For the same `op_id` with a different fingerprint, the operator MUST refuse with `IDEMPOTENCY_CONFLICT`, and no money moves.
3. The fingerprint is: player, operation type, currency, amount together with exponent, round, original, grant reference.
4. This applies while the operation is APPLIED. An `op_id` that the operator has rolled back, or remembers as rolled back, answers per item 2.1, whatever the fingerprint.
5. The same `op_id` with 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:

```json
{
  "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
    }
  }
}
```
```json
{
  "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**

1. 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.
2. A blocked or disabled player can't place NEW bets. The operator MUST still accept Credit and Rollback for such a player.
3. The operator MUST NOT refuse Credit and Rollback for limits.
4. 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.
5. 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`)
6. After any other refusal of a Credit, GA resends it.
7. 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 with `ALREADY_ROLLED_BACK`. It accepts Rollback and Reconcile in any round state.

**Example**—the player's session ended hours ago, but the win still arrives:

```json
{
  "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
  }
}
```
```json
{
  "status": "ok",
  "data": {
    "operator_tx_id": "tx-9002",
    "balance_after": {
      "currency": "EUR",
      "amount": 2500,
      "exponent": 2
    }
  }
}
```

---

### 2.5 Reconcile

**Normative text**

1. Reconcile is a read. It MUST always return an ok envelope.
2. The operator derives the state from its record of `original_op_id`:

   | Operator's record | Reconcile state |
   |---|---|
   | absent | `NOT_APPLIED` |
   | still being committed | `UNKNOWN` |
   | applied | `APPLIED` |
   | rolled back / remembered as rolled back | `NOT_APPLIED` |

3. Reconcile MUST NOT record anything. It's never a barrier.
4. The operator MUST NOT return `UNKNOWN_ORIGINAL` to a Reconcile.
5. For `NOT_APPLIED` of a Credit, GA's reading is "resend it".

**Example**—the operator never saw `op-bet-90`:

```json
{
  "request_id": "req-0050",
  "action": "reconcile",
  "payload": {
    "meta": {
      "request_id": "req-0050",
      "op_id": "op-rc-12"
    },
    "original_op_id": "op-bet-90"
  }
}
```
```json
{
  "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**

1. The operator MUST serialise per `op_id`: it inserts a unique key before business logic runs.
2. The second request MUST wait for the commit of the first.
3. If its fingerprint matches, the second request MUST receive the same response as the first. Otherwise it receives `IDEMPOTENCY_CONFLICT`.
4. The operator never applies the operation a second time.
5. The "same response" rule concerns operations that WERE processed. A refused request leaves the `op_id` unused, 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:

```json
{
  "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**

1. Both sides MUST keep `op_id` records for at least 4 months. The 7-day window of the published v1 wallet spec remains the hard minimum for v1 bridges.
2. 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. A `Retry-After` may 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.
3. 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.
4. When the 72 hours are over, GA sets `manual_review` and raises an alert. The state stays unknown, and GA never marks it "failed".
5. Every attempt carries a new `request_id` and the same `op_id`.
6. 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>
```
```json
{
  "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"
  }
}
```
```json
{
  "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**

1. Money is `{currency, amount:int64, exponent:0..18}`.
2. 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`.
3. The operator MUST refuse an exponent mismatch or an exponent outside 0..18 with `CURRENCY_MISMATCH`.
4. The operator MUST refuse an int64 overflow with `INVALID_REQUEST`.
5. Both refusals happen before money moves, and the `op_id` stays unused.
6. The operator MUST ignore unknown fields.
7. The operator MUST refuse an unknown enum value in a known field with `INVALID_REQUEST`.

**Example**—the operator holds EUR with exponent 2:

```json
{
  "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
    }
  }
}
```
```json
{
  "status": "error",
  "error": {
    "code": "ERROR_CODE_CURRENCY_MISMATCH",
    "message": "exponent mismatch",
    "retryable": false
  }
}
```

---

### 2.9 Rollback matching, DebitCredit, balance-reducing corrections

**Normative text**

1. The operator MUST execute a Rollback only if its player and currency match the original's. Otherwise it refuses the Rollback terminally with `INVALID_REQUEST` or `CURRENCY_MISMATCH`, and no money moves.
2. A Rollback of a Debit is always full: `money` equals the stored amount of the original. A partial rollback exists only for a Credit, through `partial` / `debit_mode` / `debited`.
3. `DebitCredit` MUST 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.
4. A provider correction (adjustment) that REDUCES the player's balance arrives as an ordinary `Debit`, idempotent by an `op_id` that GA derives from the provider's own correction id. One that INCREASES it arrives as a `Credit` of kind `ADJUSTMENT`. The correction names no earlier operation and rolls nothing back.
5. 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.
6. 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:

```json
{
  "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
    }
  }
}
```
```json
{
  "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**

1. GA has two free-round mechanisms. They never share a callback.
2. **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, and `SettleGrant` pays it ONCE (carrying `grant_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, `SettleGrant` is the only money call the operator receives.
3. **Provider-issued promo.** The operator receives, per winning spin, an ordinary `Credit` of kind `FREE_ROUND` carrying the campaign reference (`campaign_ref`), or one `SettleGrant` at the end.
4. `IssueGrant` is idempotent by the operator's `idempotency_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.
5. You can cancel only an ACTIVE grant. A repeated cancel returns the same result. GA allows cancelling a partly played ACTIVE grant.
6. A grant has a hard end date (`valid_until`). GA refuses a free round consumed after that date.
7. 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). `SettleGrant` pays it for the rounds actually played. When the win arrives after settlement, a separate `Credit` of kind `FREE_ROUND` carrying 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.
8. **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.
9. Credits of kind `BONUS`, `PROMO`, `JACKPOT`, `TOURNAMENT`, `CASHBACK`, `ADJUSTMENT`, `FREE_ROUND` are ordinary Credits: items 2.2–2.7 apply in full. They're idempotent by `op_id`. The operator never refuses them for session, block, or limits. GA resends them until delivered and never rolls them back.
10. **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.
11. The operator answers success to a provider's rollback of a free-round bet. The rollback moves no money, because the bet took none. `SettleGrant` is never a Rollback target.
12. `SettleGrant` is one per grant and idempotent by `op_id`. On an unknown outcome, GA resends it per item 2.7 until acknowledged.

**Example**—a grant is exhausted, and GA settles it once:

```json
{
  "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
  }
}
```
```json
{
  "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**

1. 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.
2. **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.
3. **Credit / SettleGrant.** GA resends with the same `op_id` until 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.
4. **Rollback.**
   - GA resends with the same `op_id` per 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_review` and raises an alert.
5. 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.
6. There is one Rollback per original, and GA reads its amount from the stored original.
7. 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:

```json
{
  "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
    }
  }
}
```
```json
{
  "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 `int64` in minor units plus an explicit `exponent`, 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 |

---
