Tournaments
Beta
Tournaments are in beta. The API may change before they leave it.
A tournament is a scheduled event on your token page, run by one of your page's Crown Apps. Players register, your game server reports who placed where, and once the event has closed and a review hold has passed, Chain Daddy pays the prize table in your token from a prize pool you fund from your own wallet. The winners sign nothing and pay no gas.
A tournament adds three things to a prize pool: a time window, the entry options you choose, and one payout of the whole table after a review hold.
You're the sponsor. Prizes must be won by skill or by taking part, never by chance, and entry is never paid for. The rules you publish are the event's rules. See the terms.
How it works
- You create an event on your token page: the app it runs in, the prize pool that pays it, the prize table, the window, the rules and who may enter. It starts as a draft.
- You schedule it. Players can register from the token page.
- It goes live at its start. Your game server posts the results, as often as it likes, until the payout.
- It closes at its end. The review hold starts: 24 hours unless you set another length. During the hold you can disqualify an entry, with a reason.
- The prize table is paid when the hold ends, to the best ranked entries that were not disqualified, through your prize pool's own awards.
The token page shows the event, its entry count and, once it is paid, the winners with their transactions. It never shows the full standings: your app shows its own standings to its players.
Before you start
- A prize pool on your token, in your wallet, with enough budget left for the whole prize table. Its Max per award must cover your biggest prize.
- Your token's store, which the prizes are paid through (who can open a store).
- The app installed on your token page.
The event
| Field | |
|---|---|
appSlug | The app the event runs in, installed on the page |
title | Up to 120 characters |
rules | Required, up to 5000 characters. Published with the event |
startsAt, endsAt | The window, in RFC 3339. At most 90 days |
holdHours | The review hold after the end, 0 to 168. Default 24 |
minPlayers | The fewest ranked players for the prizes to be paid. Default 4 |
prizeTable | [{"place": 1, "amount": "500000000000000000000"}, …]: places 1 to N, each once, N at most 20. Amounts are base units of your token |
campaignId | Your prize pool |
eligibility | Who may enter, below |
resultsKeyId | The store key your game server posts results with, below. Without one, only you can post them |
Who may enter
{"holdersOnly": {"minBalance": "1000000000000000000000"}, "oneEntryPerWallet": true, "verifiedIdentity": false}| Option | |
|---|---|
holdersOnly | Only wallets holding at least minBalance base units of your token may register. null for an event open to everyone |
oneEntryPerWallet | On by default. The platform always keeps one registration per wallet; this tells your app whether a wallet may also play only once |
verifiedIdentity | A player must have an X, Discord, GitHub, Twitch, YouTube or Kick account proven on their wallet, and one proven account enters once: a second wallet with the same account is refused |
The wallet that owns the prize pool can't enter its own event.
Posting results
Your game server posts the results with a store key that has the tournaments:results scope, and which you pinned on the event as its resultsKeyId. That scope opens this one route and nothing else: not an award, not a purchase, not the store's reads. Create a separate key for it.
curl -X PUT https://api.chaindaddy.io/api/v2/tournaments/$TOURNAMENT_ID/results \
-H "Authorization: Bearer $CHAINDADDY_RESULTS_KEY" \
-H "Content-Type: application/json" \
-d '{"results": [
{"wallet": "0x5a1c…", "rank": 1, "metric": "1:02.381"},
{"wallet": "0x7d20…", "rank": 2, "metric": "1:03.004"}
]}'
# → {"tournamentId": "…", "accepted": 2}| Field | |
|---|---|
wallet | A wallet that registered for the event |
rank | 1 or more. Lower is better; ties are allowed |
metric | Optional, up to 64 characters: what your game measured (a time, a score). Only you see it |
Every post replaces the one before, so post the whole standings each time. A wallet you leave out is unranked. You can post while the event is live and during the review hold, until the payout starts. Your own account (a session or your account key) can post too.
Take each wallet from a sign-in you have verified, never from a value a game client sends you. In a Crown App, that is the app's session token.
Errors
Errors are {"error": "…", "code": "…"}.
| Status | code | What to do |
|---|---|---|
| 400 | INVALID_REQUEST | Fix the body: a wallet twice, a rank under 1, a metric too long, an unknown field |
| 400 | WALLET_NOT_REGISTERED | wallets lists the ones that did not register. Nothing was written |
| 401 | INVALID_STORE_KEY | The key is wrong or revoked |
| 403 | TEST_KEY_NOT_ALLOWED | A test store key (cd_iap_test_…) never drives a real payout. Use the live store's key |
| 403 | SCOPE_REQUIRED | The key lacks tournaments:results |
| 403 | KEY_NOT_PINNED | The event's resultsKeyId is another key |
| 403 | IAP_PLAN_REQUIRED | The wallet that opened the store no longer has the plan its keys need |
| 404 | NOT_FOUND | No such event |
| 409 | TOURNAMENT_STATE | The event is not live or in its hold: not started, paying, paid or cancelled |
The payout
When the hold ends:
- Fewer than
minPlayersranked players (disqualified entries don't count): the event endspaidwith no winners, andpayoutNotesays why. - Otherwise place 1 goes to the best rank, place 2 to the next, and so on, skipping disqualified entries. Two entries on the same rank are ordered by who registered first. If fewer players ranked than there are places, the lower places are not paid.
The places are paid as your prize pool's awards, all together or not at all: every limit of the pool is checked for the whole table at once. If the pool can't cover it (its budget, its max per award, its awards left, its per-wallet or 24-hour limits, or it is paused), no place is paid, the event waits in needs_funding, and payoutNote says what to fix. Fix it, then retry the payout from the Events tab, or with POST /api/v2/tournaments/{id}/retry-payout.
Each place's award has the Idempotency-Key
<tournamentId>:<wallet>:place-<n>so a payout that runs twice pays nothing twice. Your store's webhook hears each prize as iap.airdrop.award.submitted, …confirmed or …failed, with that key in data.payload.idempotencyKey, exactly like a prize your server pays itself (what happens next).
Statuses
| Status | Means |
|---|---|
draft | Only you and your page's managers see it. You can edit it |
scheduled | Published. Players can register |
live | Between its start and its end. Players can register, and results are accepted |
closed | The review hold. Results are accepted, and you can disqualify |
paying | The payout is running. Results and disqualifications are closed |
needs_funding | The prize pool couldn't cover the table, and nothing was paid. Fix it and retry, or cancel |
paid | Done. The winners are public |
cancelled | Stopped before its payout. Nothing was paid |
The API
All under https://api.chaindaddy.io/api/v2. "The creator" is the wallet that owns the event's prize pool, while it manages the token page.
| Route | Who | Does |
|---|---|---|
GET /tournaments?crown=&chain= | Anyone | The page's events, newest first. includeDrafts=true adds drafts, for the page's managers |
GET /tournaments/{id} | Anyone | One event. winners (place, wallet, amount, txHash, status) only once it is paid. With a sign-in, registered says whether you entered |
GET /tournaments/{id}/entries | The page's managers | Every entry with its rank, metric and disqualification |
POST /tournaments | The creator | Create a draft: crownId, chain and the fields above |
PUT /tournaments/{id} | The creator | Edit a draft |
POST /tournaments/{id}/schedule | The creator | Publish a draft. The prize pool is checked again |
POST /tournaments/{id}/cancel | The creator, or the page's owner | Any status before the payout, or needs_funding |
POST /tournaments/{id}/register | A signed-in player | Register a wallet on the event's chain ({"wallet"} to pick one). Registering again returns the entry |
PUT /tournaments/{id}/results | The pinned key, or the creator | Posting results |
POST /tournaments/{id}/disqualify | The creator | {"wallet", "reason"}, during the hold |
POST /tournaments/{id}/retry-payout | The creator | After needs_funding |
Creating or scheduling an event is refused when the prize table can't be paid: PRIZE_POOL_INVALID (not a prize pool of this token), PRIZE_POOL_NOT_YOURS, PRIZE_OVER_MAX_AWARD, PRIZE_TABLE_OVER_BUDGET, STORE_REQUIRED, APP_NOT_INSTALLED or RESULTS_KEY_INVALID. Registering is refused with HOLDERS_ONLY, VERIFIED_IDENTITY_REQUIRED, IDENTITY_ALREADY_ENTERED or SPONSOR_CANNOT_ENTER.
Related docs
- Token Prizes: the prize pool a tournament pays from
- Store and Rewards: store keys and your store's webhook
- Security: verifying who a player is