Exchanges
How players convert Red into Blue or into a testnet token and back, what each exchange status means, and how you read, reconcile and release exchanges.
An exchange converts value a player holds into something else. Players start exchanges themselves from the Player surface; your integration reads them and acts on the few that need help. Every exchange is one durable record, created before anything moves, and every balance change it makes is a ledger posting committed together with the record.
Kinds
kind | From | To | Available |
|---|---|---|---|
internal | red | blue, or back | Always, once you have published both currencies |
crypto-payout | red | A testnet native token (eth, hbar or sol) sent to the player's address | When the chain advertises exchange in GET /v1/web3/capabilities |
crypto-deposit | A native token the player sends | red | When the chain advertises exchange |
The internal rate is fixed: five Blue per Red, one Red per five Blue. Arithmetic is on integers
only, so a Blue amount that is not a multiple of five is refused before anything is written. Crypto
rates are configured on the deployment and pinned on the record at the moment it is created. Any
sub-unit remainder the conversion cannot represent is reported in dust and never credited.
Crypto legs run on testnets only (Ethereum Sepolia, Hedera testnet, Solana). Fiat exchange is not offered: cash leaves the platform only as a payout.
Statuses
flowchart LR
subgraph Payout
R[reserved] --> S[submitted]
S --> OK1[settled]
S --> F[failed]
F --> S
R --> REL1[released]
F --> REL1
S --> U1[unresolved]
end
subgraph Deposit
I[intended] --> OK2[settled]
I --> REL2[released]
I --> U2[unresolved]
end
| Status | Meaning |
|---|---|
settled | Done. For a payout, the transfer was proven from chain state. |
rejected | An internal exchange that could not be made; nothing moved. |
reserved | Payout: the Red is debited into a reserve; no transaction is signed yet. |
submitted | Payout: a signed transaction exists and its outcome is not yet known. |
failed | Payout: the attempt failed or expired. The next reconcile opens exactly one new attempt. |
intended | Deposit: the player has an address and a nonce to pay into, and nothing has arrived yet. |
released | The reservation or intent was cancelled and any reserved Red credited back. Final. |
unresolved | A transaction succeeded but did not prove the expected transfer. Final; the Red stays reserved because value may have moved. |
internal exchanges settle or are rejected in one step. settled, rejected, released and
unresolved are final.
Read exchanges
GET /v1/exchanges with scope rewards:read lists your tenant's exchanges, newest first.
| Query parameter | Rule |
|---|---|
playerId | Optional; one player |
kind | internal, crypto-payout or crypto-deposit |
status | Any status above |
chain | solana, ethereum or hedera |
limit | 1 to 100, default 50 |
There is no cursor. An invalid filter is refused with 400 EXCHANGE_QUERY_INVALID.
curl "$REWARD_API_URL/v1/exchanges?kind=crypto-payout&status=submitted" \
-H "Authorization: Bearer $TANITAR_API_KEY"GET /v1/exchanges/:exchangeId reads one. An exchange of another tenant answers
404 EXCHANGE_NOT_FOUND, the same as one that does not exist.
{
"exchangeId": "exc_4b1e…",
"playerId": "player-7f3a",
"kind": "crypto-payout",
"status": "settled",
"from": { "currencyKey": "red", "amount": "200" },
"to": { "currencyKey": "eth", "amount": "2000000000000000" },
"rate": "10000000000000",
"dust": "0",
"gameSlug": null,
"chain": "ethereum",
"clusterGenesisHash": null,
"destination": "0x5B38Da6a701c568545dCfcB03FcB875f56beddC4",
"treasury": null,
"nonce": null,
"txId": "0x9f2c…",
"attemptCount": 1,
"failureCode": null,
"slot": null,
"confirmedAt": "2026-09-30T11:02:09.412Z",
"settledAt": "2026-09-30T11:02:10.004Z",
"requestedBy": "player-7f3a",
"createdAt": "2026-09-30T11:01:31.771Z",
"updatedAt": "2026-09-30T11:02:10.004Z"
}Amounts are decimal strings; token amounts are in the chain's base units. The record never contains signer material or node addresses.
Reconcile a payout
POST /v1/exchanges/:exchangeId/reconcile with scope rewards:mutate drives a crypto-payout
forward:
reserved: sends the first attempt.submitted: checks the retained transaction on chain first. If it confirmed, the exchange settles; if it is still pending, you get409 EXCHANGE_PENDINGand nothing is re-sent; if it failed or expired, the record is markedfailedand exactly one new attempt is sent.failed: sends exactly one new attempt.settled,released,unresolved, or aninternalexchange: returns the record unchanged.
A deposit is confirmed by the player, who supplies the transaction id that funded it. Reconciling an
intended deposit from your integration is refused with EXCHANGE_DEPOSIT_NOT_PROVEN, because no
transaction id is supplied.
Release
POST /v1/exchanges/:exchangeId/release with scope rewards:mutate cancels:
- a
crypto-payoutinreservedorfailed, crediting the reserved Red back to the player; - a
crypto-depositinintendedthat was never funded.
Releasing an already released exchange returns it unchanged. Any other state is refused with
EXCHANGE_NOT_RELEASABLE. A submitted payout cannot be released, because its transaction may
still land: reconcile it instead.
What your players see
Players quote, create, confirm and release their own exchanges on the Player surface:
GET /player/v1/exchanges/quote, POST /player/v1/exchanges, POST /player/v1/exchanges/:exchangeId/confirm,
POST /player/v1/exchanges/:exchangeId/release, and GET /player/v1/exchanges for their history.
Creating an exchange requires an Idempotency-Key, so a player's retry never exchanges twice.
When an exchange settles, reward.exchange-settled is published, the player gets an inbox
notification, and your webhook endpoint can receive the event (see the
event catalog).
Award kinds and awards
The six system award kinds, your own award-kind registry, award definitions, and how awards are granted, read, resolved and optionally minted on a testnet.
Payouts
Pay cash prizes from your own Stripe Connect account to your players' connected accounts, without Tanitar ever holding the funds.