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

# Wallet integration

> The five endpoints you implement, and the money rules that govern them.

This is the half of the integration you build. Zero-Dash calls these endpoints; your platform is the server.

<Card title="You implement five endpoints" icon="server" horizontal>
  `GET /player` · `POST /debit` · `POST /credit` · `POST /rollback` · `GET /players/{accountId}/transactions/{transactionId}`
</Card>

Mount them under any base path you like. Whatever full path you register is the path that gets signed — a prefix such as `/zerodash/v1` is fine, as long as you verify against the same path.

## Before you write code

<CardGroup cols={2}>
  <Card title="Signature verification" icon="key" href="/security/signature">
    Recompute and compare before touching the wallet. Non-negotiable.
  </Card>

  <Card title="IP allowlist" icon="network-wired" href="/security/ip-allowlist">
    Only two source addresses may reach these endpoints.
  </Card>

  <Card title="Amounts and currencies" icon="coins" href="/wallet/amounts">
    Decimals on the wire, minor units in storage, 8 places for crypto.
  </Card>

  <Card title="Idempotency" icon="fingerprint" href="/wallet/idempotency">
    The same `transactionId` must never apply twice.
  </Card>
</CardGroup>

## The endpoints

| Endpoint                                                                    | When it fires                 | What you do                                     |
| --------------------------------------------------------------------------- | ----------------------------- | ----------------------------------------------- |
| [`GET /player`](/api-reference/callbacks/player)                            | Once, when a game opens       | Validate the token, return identity and balance |
| [`POST /debit`](/api-reference/callbacks/debit)                             | Once per bet                  | Subtract the stake, return the new balance      |
| [`POST /credit`](/api-reference/callbacks/credit)                           | Once per settled round        | Add the payout, return the new balance          |
| [`POST /rollback`](/api-reference/callbacks/rollback)                       | Instead of credit, on failure | Return the stake, return the new balance        |
| [`GET /players/…/transactions/…`](/api-reference/callbacks/get-transaction) | During reconciliation         | Report the state of one transaction             |

## The round contract

```mermaid theme={null}
stateDiagram-v2
    direction LR
    [*] --> Bet: POST /debit
    Bet --> Settled: POST /credit
    Bet --> Reverted: POST /rollback
    Settled --> [*]
    Reverted --> [*]

    note right of Bet
        transactionId identifies the bet
        gameRoundId groups the round
    end note
```

Three rules, and every reconciliation question follows from them:

<Steps>
  <Step title="Exactly one debit opens a round" icon="1">
    Identified by `transactionId`. Its `gameRoundId` is the round key.
  </Step>

  <Step title="Exactly one settlement closes it" icon="2">
    A credit **or** a rollback. Never both, never neither. The settlement carries `referenceTransactionId` pointing back at the debit, and the same `gameRoundId`.
  </Step>

  <Step title="Every settlement is eventually delivered" icon="3">
    Credits and rollbacks are retried for up to 3 days, then escalated to manual review. A round does not get silently dropped.
  </Step>
</Steps>

## The four rules that break integrations

<AccordionGroup>
  <Accordion title="A losing round still sends a credit" icon="circle-half-stroke">
    When the player loses, you receive `POST /credit` with `payout.amount` of `0`. This is not an error and not an edge case — it is how a round closes.

    Create the transaction, credit zero, return the unchanged balance and `200 OK`. Rejecting zero payouts leaves rounds open forever and puts every one of them into our retry queue.
  </Accordion>

  <Accordion title="Credit and rollback must work with no player session" icon="clock">
    Neither carries a token. Both can arrive days after the round, long after the player logged out, and `/credit` will keep being retried until it succeeds.

    Do not look up a session. Do not require the player to be online. Resolve the player from `playerId` and apply the movement.
  </Accordion>

  <Accordion title="Free rounds debit a stake of zero" icon="gift">
    When `reason` is `freeround`, `stake.amount` is `0`. Subtract nothing — but **still create the transaction** and return a valid response with the current balance.

    The matching credit carries the winnings with the stake already deducted. Credit exactly what you are given. Each free spin is its own bet, with its own debit and its own credit.
  </Accordion>

  <Accordion title="422 and 500 on debit mean opposite things" icon="triangle-exclamation">
    | Code  | Meaning                                                   | What we do                        |
    | ----- | --------------------------------------------------------- | --------------------------------- |
    | `422` | The transaction **certainly does not exist** on your side | Nothing. No rollback is sent.     |
    | `500` | The transaction **was created**, then something failed    | We send a rollback, with retries. |

    Return `500` when nothing was written and you get a rollback for a transaction you never had. Return `422` when the money did move and you never get the correction. Choose deliberately.
  </Accordion>
</AccordionGroup>

## Response shape

Debit, credit, rollback and the transaction lookup all answer with the same object:

```json theme={null}
{
  "operatorTransactionId": "op-tx-55901",
  "balance": {
    "amount": 95.00,
    "currency": "USD",
    "updatedAt": "2026-01-29T14:05:29.678Z"
  },
  "createdAt": "2026-01-29T14:05:29.678Z"
}
```

<ResponseField name="operatorTransactionId" type="string" required>
  Your internal transaction ID. Stored by Zero-Dash for troubleshooting only — we never use it as a key.
</ResponseField>

<ResponseField name="balance" type="object" required>
  The player's balance **after** the operation was applied. A decimal value with its ISO-4217 currency. See [Amounts and currencies](/wallet/amounts).
</ResponseField>

<ResponseField name="createdAt" type="string">
  ISO-8601 UTC creation time of the transaction. Defaults to the processing time when omitted.
</ResponseField>

<Warning>
  `balance` is the balance **after** the movement, not before it. Games display it directly to the player, so an off-by-one-transaction value is visible immediately.
</Warning>

## Implementation checklist

<Steps>
  <Step title="Verify before you act" icon="shield-halved">
    Signature, timestamp freshness, source IP. Reject with `401` and touch nothing.
  </Step>

  <Step title="Look up transactionId first" icon="fingerprint">
    Already processed? Return the stored result with `200 OK`, without reapplying. See [Idempotency](/wallet/idempotency).
  </Step>

  <Step title="Apply the movement atomically" icon="database">
    Write the transaction row and update the balance in a single transaction, with a unique constraint on `transactionId`.
  </Step>

  <Step title="Answer with the post-movement balance" icon="reply">
    Decimal amount, currency, and your internal transaction ID.
  </Step>

  <Step title="Choose failure codes deliberately" icon="triangle-exclamation">
    `409` insufficient funds, `422` nothing was written, `500` something was. See [Errors and retries](/wallet/errors-and-retries).
  </Step>
</Steps>
